tina4ruby 3.13.94 → 3.13.96
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +883 -0
- data/README.md +1 -1
- data/lib/tina4/auth.rb +166 -87
- data/lib/tina4/auto_crud.rb +29 -32
- data/lib/tina4/cache_backends/base_backend.rb +19 -0
- data/lib/tina4/cache_backends/database_backend.rb +29 -0
- data/lib/tina4/cache_backends/memcached_backend.rb +124 -13
- data/lib/tina4/cache_backends/memory_backend.rb +15 -0
- data/lib/tina4/cache_backends/redis_backend.rb +173 -52
- data/lib/tina4/cache_backends.rb +10 -1
- data/lib/tina4/cli.rb +23 -39
- data/lib/tina4/cors.rb +186 -30
- data/lib/tina4/database/sqlite3_adapter.rb +4 -1
- data/lib/tina4/database.rb +322 -22
- data/lib/tina4/database_adapter.rb +178 -0
- data/lib/tina4/database_result.rb +63 -17
- data/lib/tina4/database_url.rb +363 -0
- data/lib/tina4/dev.rb +0 -1
- data/lib/tina4/dev_admin.rb +118 -20
- data/lib/tina4/dispatch_pipeline.rb +605 -0
- data/lib/tina4/docstore.rb +274 -60
- data/lib/tina4/drivers/firebird_driver.rb +118 -4
- data/lib/tina4/drivers/mongodb_driver.rb +19 -4
- data/lib/tina4/drivers/mssql_driver.rb +73 -10
- data/lib/tina4/drivers/mysql_driver.rb +71 -4
- data/lib/tina4/drivers/odbc_driver.rb +40 -4
- data/lib/tina4/drivers/postgres_driver.rb +97 -10
- data/lib/tina4/drivers/sqlite_driver.rb +21 -2
- data/lib/tina4/env.rb +176 -34
- data/lib/tina4/field_types.rb +12 -0
- data/lib/tina4/health.rb +30 -14
- data/lib/tina4/job.rb +15 -5
- data/lib/tina4/log.rb +236 -32
- data/lib/tina4/mcp.rb +11 -5
- data/lib/tina4/messenger.rb +248 -36
- data/lib/tina4/metrics.rb +179 -891
- data/lib/tina4/middleware.rb +191 -56
- data/lib/tina4/migration.rb +17 -1
- data/lib/tina4/orm.rb +114 -17
- data/lib/tina4/public/css/tina4.min.css +1 -1
- data/lib/tina4/queue.rb +154 -9
- data/lib/tina4/queue_backends/kafka_backend.rb +191 -2
- data/lib/tina4/queue_backends/lite_backend.rb +121 -25
- data/lib/tina4/queue_backends/mongo_backend.rb +146 -10
- data/lib/tina4/queue_backends/rabbitmq_backend.rb +194 -1
- data/lib/tina4/rack_app.rb +94 -316
- data/lib/tina4/request.rb +48 -8
- data/lib/tina4/response.rb +42 -1
- data/lib/tina4/response_cache.rb +142 -24
- data/lib/tina4/router.rb +141 -12
- data/lib/tina4/session.rb +243 -29
- data/lib/tina4/session_handlers/database_handler.rb +185 -20
- data/lib/tina4/session_handlers/file_handler.rb +113 -21
- data/lib/tina4/session_handlers/memcached_handler.rb +183 -0
- data/lib/tina4/session_handlers/mongo_handler.rb +232 -15
- data/lib/tina4/session_handlers/mongo_wire_client.rb +300 -0
- data/lib/tina4/session_handlers/redis_handler.rb +20 -6
- data/lib/tina4/session_handlers/valkey_handler.rb +18 -4
- data/lib/tina4/shutdown.rb +180 -30
- data/lib/tina4/sql_translator.rb +110 -0
- data/lib/tina4/swagger.rb +50 -18
- data/lib/tina4/version.rb +1 -1
- data/lib/tina4/webserver.rb +28 -6
- data/lib/tina4.rb +289 -37
- metadata +35 -17
- data/lib/tina4/scss_compiler.rb +0 -349
data/lib/tina4/log.rb
CHANGED
|
@@ -29,28 +29,105 @@ module Tina4
|
|
|
29
29
|
# ANSI escape code regex for stripping from file output
|
|
30
30
|
ANSI_RE = /\033\[[0-9;]*m/
|
|
31
31
|
|
|
32
|
+
# The logger must never be surprised by what it is handed. Console lines are
|
|
33
|
+
# capped; control characters never reach a terminal. Same numbers in all four
|
|
34
|
+
# frameworks (feature 2 of the feature audit).
|
|
35
|
+
STDOUT_MAX_CHARS = 2000
|
|
36
|
+
CONTROL_CHARS = /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/
|
|
37
|
+
|
|
32
38
|
# Defaults used when env vars are unset.
|
|
33
39
|
DEFAULT_ROTATE_SIZE = 10 * 1024 * 1024 # 10MB
|
|
34
40
|
DEFAULT_ROTATE_KEEP = 5
|
|
35
41
|
|
|
42
|
+
# The error classes TINA4_LOG_STRICT must let ESCAPE stdlib ::Logger.
|
|
43
|
+
#
|
|
44
|
+
# TINA4_LOG_STRICT is documented as "raise on a log write failure instead of
|
|
45
|
+
# swallowing", and #write_to_file dutifully rescues IOError/SystemCallError
|
|
46
|
+
# and re-raises when @strict. It was a NO-OP anyway: ::Logger::LogDevice
|
|
47
|
+
# wraps every device write in its own `handle_write_errors`, which rescues
|
|
48
|
+
# and turns the failure into a bare `warn` on stderr. The real error was
|
|
49
|
+
# swallowed one layer BELOW Tina4 and never reached Tina4's rescue at all.
|
|
50
|
+
#
|
|
51
|
+
# MEASURED 2026-08-01 on a genuinely full 1MB HFS+ ram disk (0 KB free),
|
|
52
|
+
# Ruby 4.0.2: with TINA4_LOG_STRICT=true, Tina4::Log.info(...) printed
|
|
53
|
+
# "log writing failed. No space left on device @ rb_sys_fail_on_write" to
|
|
54
|
+
# stderr and returned normally. The operator got a stderr warning and strict
|
|
55
|
+
# mode did nothing.
|
|
56
|
+
#
|
|
57
|
+
# ::Logger has a first-class seam for exactly this — `reraise_write_errors:`
|
|
58
|
+
# (logger >= 1.5.0), which handle_write_errors re-raises through instead of
|
|
59
|
+
# warning. Listing the two classes #write_to_file already rescues keeps the
|
|
60
|
+
# two ends of the strict path in agreement. Note Errno::ENOSPC (and every
|
|
61
|
+
# other errno) is a SystemCallError, so the real disk-full case is covered.
|
|
62
|
+
STRICT_WRITE_ERRORS = [IOError, SystemCallError].freeze
|
|
63
|
+
|
|
64
|
+
# A log device that writes ONLY log lines.
|
|
65
|
+
#
|
|
66
|
+
# stdlib ::Logger stamps a banner as the FIRST LINE of every file it creates
|
|
67
|
+
# — and it creates one on every rotation too:
|
|
68
|
+
#
|
|
69
|
+
# # Logfile created on 2026-08-01 21:49:10 +0200 by logger.rb/v1.7.0
|
|
70
|
+
#
|
|
71
|
+
# In JSON mode (TINA4_LOG_FORMAT=json) that is not JSON, so any line-oriented
|
|
72
|
+
# shipper — Filebeat, Fluent Bit, Vector, promtail, `jq -c` — fails on line 1
|
|
73
|
+
# of every file and every rotated file. It is a Ruby-only artifact: no other
|
|
74
|
+
# Tina4 framework emits it, so a JSON pipeline that works on Python/PHP/Node
|
|
75
|
+
# breaks here and nowhere else.
|
|
76
|
+
#
|
|
77
|
+
# Suppressing it in the DEVICE (rather than pre-creating the file) covers
|
|
78
|
+
# rotation as well, and keeps ::Logger's own rotation/locking untouched —
|
|
79
|
+
# add_log_header is the single method whose whole body is that banner.
|
|
80
|
+
class HeaderlessLogDevice < ::Logger::LogDevice
|
|
81
|
+
def add_log_header(file); end
|
|
82
|
+
end
|
|
83
|
+
|
|
36
84
|
class << self
|
|
37
85
|
attr_reader :log_dir, :log_file_path
|
|
38
86
|
|
|
39
|
-
|
|
40
|
-
|
|
87
|
+
# configure(target = nil)
|
|
88
|
+
#
|
|
89
|
+
# Logs land in a `logs/` folder by default. The argument OVERRIDES that,
|
|
90
|
+
# and it accepts a DIRECTORY or a FILE PATH:
|
|
91
|
+
#
|
|
92
|
+
# configure -> ./logs/tina4.log + ./logs/error.log
|
|
93
|
+
# configure("/var/log/myapp") -> /var/log/myapp/tina4.log + error.log
|
|
94
|
+
# configure("/var/log/myapp/app.log") -> that exact file (no error.log sibling)
|
|
95
|
+
# TINA4_LOG_DIR=log -> ./log/
|
|
96
|
+
#
|
|
97
|
+
# A target with a file extension is a file path; anything else is a
|
|
98
|
+
# directory (an existing directory is always treated as one, extension or
|
|
99
|
+
# not). Naming a file means "one file at this path", so no error.log
|
|
100
|
+
# appears beside it -- same rule TINA4_LOG_FILE already followed.
|
|
101
|
+
#
|
|
102
|
+
# BREAKING for Ruby callers: the argument used to be a project ROOT with
|
|
103
|
+
# `logs/` appended, so `configure("/app")` wrote to /app/logs/ while the
|
|
104
|
+
# identical call on Python and PHP wrote to /app/. That is the same
|
|
105
|
+
# file-versus-directory confusion feature 1 found in loadEnv, and it made
|
|
106
|
+
# "put the logs exactly here" impossible to express. If you relied on the
|
|
107
|
+
# old behaviour, pass the parent explicitly: `configure(File.join(root, "logs"))`.
|
|
108
|
+
def configure(target = nil)
|
|
109
|
+
# Explicit argument wins, then TINA4_LOG_DIR, then ./logs.
|
|
41
110
|
log_dir_env = ENV["TINA4_LOG_DIR"]
|
|
42
|
-
log_dir_env =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
111
|
+
log_dir_env = nil if log_dir_env && log_dir_env.empty?
|
|
112
|
+
chosen = target || log_dir_env || "logs"
|
|
113
|
+
chosen = File.join(Dir.pwd, chosen) unless File.absolute_path?(chosen)
|
|
114
|
+
|
|
115
|
+
if target_is_file?(chosen)
|
|
116
|
+
@log_dir = File.dirname(chosen)
|
|
117
|
+
explicit_target_file = chosen
|
|
118
|
+
else
|
|
119
|
+
@log_dir = chosen
|
|
120
|
+
explicit_target_file = nil
|
|
121
|
+
end
|
|
48
122
|
FileUtils.mkdir_p(@log_dir)
|
|
49
123
|
|
|
50
|
-
#
|
|
51
|
-
# Default: <log_dir>/tina4.log.
|
|
124
|
+
# A file path passed to configure() wins, then TINA4_LOG_FILE (absolute
|
|
125
|
+
# or relative to log_dir). Default: <log_dir>/tina4.log.
|
|
52
126
|
log_file_env = ENV["TINA4_LOG_FILE"]
|
|
53
|
-
|
|
127
|
+
log_file_env = nil if log_file_env && log_file_env.empty?
|
|
128
|
+
@log_file_path = if explicit_target_file
|
|
129
|
+
explicit_target_file
|
|
130
|
+
elsif log_file_env
|
|
54
131
|
File.absolute_path?(log_file_env) ? log_file_env : File.join(@log_dir, log_file_env)
|
|
55
132
|
else
|
|
56
133
|
File.join(@log_dir, "tina4.log")
|
|
@@ -61,9 +138,23 @@ module Tina4
|
|
|
61
138
|
# TINA4_LOG_ROTATE_KEEP — number of rotated backups to keep.
|
|
62
139
|
@rotate_keep = (ENV["TINA4_LOG_ROTATE_KEEP"] || DEFAULT_ROTATE_KEEP).to_i
|
|
63
140
|
|
|
64
|
-
# TINA4_LOG_FORMAT — "text" or "json".
|
|
141
|
+
# TINA4_LOG_FORMAT — "text" or "json". TEXT IS THE DEFAULT, always.
|
|
142
|
+
#
|
|
143
|
+
# Owner decision 2026-08-01: nothing but an explicit TINA4_LOG_FORMAT=json
|
|
144
|
+
# selects JSON. The implicit "production means JSON" switch is DELETED in
|
|
145
|
+
# all four frameworks, because MEASURED, "production" meant four different
|
|
146
|
+
# things and it silently picked your log format:
|
|
147
|
+
#
|
|
148
|
+
# node !isTruthy(TINA4_DEBUG) -> JSON with TINA4_DEBUG unset
|
|
149
|
+
# ruby TINA4_ENV|RACK_ENV|RUBY_ENV == "production"
|
|
150
|
+
# python only via configure(production=True)
|
|
151
|
+
# php no switch at all — JSON was the shipped default
|
|
152
|
+
#
|
|
153
|
+
# Same machine, same .env, four formats. An OBJECT (Hash/Array) passed as
|
|
154
|
+
# the message is still JSON-encoded INLINE inside the text line — that is
|
|
155
|
+
# coerce_message's job and it is unchanged.
|
|
65
156
|
format_env = ENV["TINA4_LOG_FORMAT"]
|
|
66
|
-
@format = format_env && !format_env.empty? ? format_env.downcase :
|
|
157
|
+
@format = format_env && !format_env.empty? ? format_env.downcase : "text"
|
|
67
158
|
@json_mode = @format == "json"
|
|
68
159
|
|
|
69
160
|
# TINA4_LOG_OUTPUT — "stdout", "file", or "both".
|
|
@@ -80,7 +171,10 @@ module Tina4
|
|
|
80
171
|
# be written even in production (parity with the Python master, where an
|
|
81
172
|
# explicit log_file builds a writer unconditionally), so the dev-gated
|
|
82
173
|
# default below resolves to "both" (stdout + file) rather than "stdout".
|
|
83
|
-
|
|
174
|
+
# "The operator named ONE file" — via TINA4_LOG_FILE or by passing a file
|
|
175
|
+
# path to configure(). Either way a file must be written even in
|
|
176
|
+
# production, and no error.log sibling appears next to it.
|
|
177
|
+
explicit_file = !log_file_env.nil? || !explicit_target_file.nil?
|
|
84
178
|
default_output = if explicit_file || truthy?(ENV["TINA4_DEBUG"])
|
|
85
179
|
"both"
|
|
86
180
|
else
|
|
@@ -115,14 +209,35 @@ module Tina4
|
|
|
115
209
|
# shift_size = bytes before rotation
|
|
116
210
|
# When @rotate_size is 0, omit rotation args.
|
|
117
211
|
close_file_logger
|
|
212
|
+
|
|
213
|
+
# TINA4_LOG_APPEND — append (default) or overwrite on startup.
|
|
214
|
+
#
|
|
215
|
+
# APPEND IS THE DEFAULT: a log you can lose by restarting the process is
|
|
216
|
+
# not a log. Set it false when you want one file per run (a short CLI, a
|
|
217
|
+
# test fixture, a container that ships logs elsewhere) and the file is
|
|
218
|
+
# truncated once here at configure time, never per line.
|
|
219
|
+
@append = ENV["TINA4_LOG_APPEND"].nil? || truthy?(ENV["TINA4_LOG_APPEND"])
|
|
220
|
+
|
|
118
221
|
if @output != "stdout"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
222
|
+
unless @append
|
|
223
|
+
[@log_file_path, File.join(@log_dir, "error.log")].each do |path|
|
|
224
|
+
File.write(path, "") if File.exist?(path)
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
@file_logger = build_file_logger(@log_file_path)
|
|
228
|
+
|
|
229
|
+
# Mirror WARNING and above into a dedicated error.log so
|
|
230
|
+
# `tail -f logs/error.log` gives just the stuff worth looking at.
|
|
231
|
+
# Ruby wrote ONE file where Python and PHP wrote two, so anyone whose
|
|
232
|
+
# alerting tails error.log got silence here (feature 2 of the audit,
|
|
233
|
+
# D3). Skipped when the operator named an explicit TINA4_LOG_FILE:
|
|
234
|
+
# they asked for one file at one path, so a sibling error.log
|
|
235
|
+
# appearing next to it would be a surprise.
|
|
236
|
+
@error_logger = if explicit_file
|
|
237
|
+
nil
|
|
238
|
+
else
|
|
239
|
+
build_file_logger(File.join(@log_dir, "error.log"))
|
|
240
|
+
end
|
|
126
241
|
end
|
|
127
242
|
|
|
128
243
|
@initialized = true
|
|
@@ -195,39 +310,41 @@ module Tina4
|
|
|
195
310
|
def close_file_logger
|
|
196
311
|
@file_logger&.close rescue nil
|
|
197
312
|
@file_logger = nil
|
|
313
|
+
@error_logger&.close rescue nil
|
|
314
|
+
@error_logger = nil
|
|
198
315
|
end
|
|
199
316
|
|
|
200
317
|
private
|
|
201
318
|
|
|
202
319
|
def truthy?(val)
|
|
203
|
-
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
def production?
|
|
207
|
-
env = ENV["TINA4_ENV"] || ENV["RACK_ENV"] || ENV["RUBY_ENV"] || "development"
|
|
208
|
-
env.downcase == "production"
|
|
320
|
+
Tina4::Env.is_truthy(val)
|
|
209
321
|
end
|
|
210
322
|
|
|
211
323
|
def log(level, message, context = {})
|
|
212
324
|
configure unless @initialized
|
|
213
325
|
@current_context = context.is_a?(Hash) ? context : {}
|
|
214
326
|
|
|
327
|
+
# Coerce FIRST. Anything can arrive as a message: a Hash from a handler,
|
|
328
|
+
# a binary payload off a socket, a 10MB string. See coerce_message.
|
|
329
|
+
message = coerce_message(message)
|
|
215
330
|
formatted = format_line(level, message)
|
|
216
331
|
|
|
217
332
|
# Console output respects TINA4_LOG_LEVEL and TINA4_LOG_OUTPUT
|
|
218
333
|
severity = SEVERITY_MAP[level] || 0
|
|
219
334
|
if severity >= @console_level && @output != "file"
|
|
335
|
+
# Truncate on the CONSOLE only. The file keeps the full line so a
|
|
336
|
+
# consumer parsing it loses nothing; a terminal does not need 10MB.
|
|
220
337
|
if @json_mode
|
|
221
|
-
$stdout.puts json_line(level, message)
|
|
338
|
+
$stdout.puts truncate_for_stdout(json_line(level, message))
|
|
222
339
|
else
|
|
223
|
-
$stdout.puts colorize(level, formatted)
|
|
340
|
+
$stdout.puts colorize(level, truncate_for_stdout(formatted))
|
|
224
341
|
end
|
|
225
342
|
end
|
|
226
343
|
|
|
227
344
|
# File output — always full level (consumer parses themselves) — unless disabled.
|
|
228
345
|
if @output != "stdout" && @file_logger
|
|
229
346
|
payload = @json_mode ? json_line(level, message) : strip_ansi(formatted)
|
|
230
|
-
write_to_file(payload)
|
|
347
|
+
write_to_file(payload, level)
|
|
231
348
|
end
|
|
232
349
|
|
|
233
350
|
@current_context = {}
|
|
@@ -295,7 +412,10 @@ module Tina4
|
|
|
295
412
|
fn = caller_name
|
|
296
413
|
fn_str = fn ? " [#{fn}]" : ""
|
|
297
414
|
ctx = @current_context && !@current_context.empty? ? " #{JSON.generate(@current_context)}" : ""
|
|
298
|
-
|
|
415
|
+
# Pad to 8, not 7: CRITICAL is eight characters, so a 7-wide column was
|
|
416
|
+
# broken by our own highest level. 8 is the only width that fits every
|
|
417
|
+
# level name. Cross-framework format table (feature 2 of the audit).
|
|
418
|
+
"#{ts} [#{level_str.ljust(8)}]#{rid_str}#{fn_str} #{message}#{ctx}"
|
|
299
419
|
end
|
|
300
420
|
|
|
301
421
|
def json_line(level, message)
|
|
@@ -359,10 +479,94 @@ module Tina4
|
|
|
359
479
|
"#{color}#{line}#{COLORS[:reset]}"
|
|
360
480
|
end
|
|
361
481
|
|
|
362
|
-
|
|
482
|
+
# Build one rotating file logger. stdlib Logger handles rotation natively:
|
|
483
|
+
# Logger.new(path, shift_age, shift_size) — files to keep, bytes before roll.
|
|
484
|
+
# With @rotate_size 0 the rotation args are omitted. tina4.log and error.log
|
|
485
|
+
# each get their own logger so they rotate independently.
|
|
486
|
+
# Turn anything into a single safe line of text.
|
|
487
|
+
#
|
|
488
|
+
# A String passes through when it is valid UTF-8. Binary is described
|
|
489
|
+
# rather than dumped: raw bytes at a terminal garble it and can emit
|
|
490
|
+
# escape sequences. Anything else becomes JSON, because a Hash rendered as
|
|
491
|
+
# text is the whole reason the caller logged it, falling back to inspect
|
|
492
|
+
# for a value JSON cannot represent. The logger must never be the reason a
|
|
493
|
+
# request dies.
|
|
494
|
+
def coerce_message(message)
|
|
495
|
+
text = case message
|
|
496
|
+
when String
|
|
497
|
+
if message.encoding == Encoding::BINARY || !message.valid_encoding?
|
|
498
|
+
utf8 = message.dup.force_encoding(Encoding::UTF_8)
|
|
499
|
+
return "<binary #{message.bytesize} bytes>" unless utf8.valid_encoding?
|
|
500
|
+
utf8
|
|
501
|
+
else
|
|
502
|
+
message
|
|
503
|
+
end
|
|
504
|
+
when Hash, Array
|
|
505
|
+
begin
|
|
506
|
+
JSON.generate(message)
|
|
507
|
+
rescue StandardError
|
|
508
|
+
message.inspect
|
|
509
|
+
end
|
|
510
|
+
when nil
|
|
511
|
+
""
|
|
512
|
+
else
|
|
513
|
+
message.respond_to?(:to_s) ? message.to_s : message.inspect
|
|
514
|
+
end
|
|
515
|
+
text.gsub(CONTROL_CHARS, "")
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
def truncate_for_stdout(line)
|
|
519
|
+
return line if line.length <= STDOUT_MAX_CHARS
|
|
520
|
+
"#{line[0, STDOUT_MAX_CHARS]}... (truncated, #{line.length} chars)"
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Is this target a FILE PATH or a DIRECTORY?
|
|
524
|
+
#
|
|
525
|
+
# An existing directory is always a directory, extension or not. Otherwise
|
|
526
|
+
# a basename with an extension (app.log, app.txt) is a file and anything
|
|
527
|
+
# else is a directory to create. That keeps `configure("/var/log/myapp")`
|
|
528
|
+
# a directory and `configure("/var/log/myapp/app.log")` a file without
|
|
529
|
+
# needing the path to exist yet.
|
|
530
|
+
def target_is_file?(path)
|
|
531
|
+
return false if File.directory?(path)
|
|
532
|
+
File.extname(path) != ""
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def build_file_logger(path)
|
|
536
|
+
# In strict mode ask ::Logger to let a real write error through instead
|
|
537
|
+
# of warning on stderr and returning (see STRICT_WRITE_ERRORS). Empty
|
|
538
|
+
# otherwise, which is the stdlib default, so the non-strict path behaves
|
|
539
|
+
# byte for byte as it always has.
|
|
540
|
+
reraise = @strict ? STRICT_WRITE_ERRORS : []
|
|
541
|
+
# HeaderlessLogDevice, not a bare path: ::Logger would otherwise open the
|
|
542
|
+
# file with a "# Logfile created on ..." banner that is not a log line
|
|
543
|
+
# (and not JSON). It still does the rotation — shift_age = files kept,
|
|
544
|
+
# shift_size = bytes before a roll; with @rotate_size 0 both are omitted
|
|
545
|
+
# so rotation is off. reraise_write_errors is passed to BOTH layers or
|
|
546
|
+
# the outer one swallows what the inner one raises (see STRICT_WRITE_ERRORS).
|
|
547
|
+
device = if @rotate_size > 0
|
|
548
|
+
HeaderlessLogDevice.new(path, shift_age: @rotate_keep,
|
|
549
|
+
shift_size: @rotate_size,
|
|
550
|
+
reraise_write_errors: reraise)
|
|
551
|
+
else
|
|
552
|
+
HeaderlessLogDevice.new(path, reraise_write_errors: reraise)
|
|
553
|
+
end
|
|
554
|
+
logger = ::Logger.new(device, reraise_write_errors: reraise)
|
|
555
|
+
# We do our own formatting — strip Logger's default formatter.
|
|
556
|
+
logger.formatter = proc { |_sev, _t, _p, msg| msg.to_s.end_with?("\n") ? msg : "#{msg}\n" }
|
|
557
|
+
logger
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
def write_to_file(line, level = nil)
|
|
363
561
|
return unless @file_logger
|
|
364
562
|
# Use << to bypass Logger's severity filtering — we already filtered above.
|
|
365
563
|
@file_logger << "#{line}\n"
|
|
564
|
+
# WARNING and above only. Matches the Python master and PHP, which both
|
|
565
|
+
# mirror warning+ rather than error+ -- error.log is "the stuff worth
|
|
566
|
+
# looking at", and a warning qualifies.
|
|
567
|
+
if @error_logger && level && (SEVERITY_MAP[level] || 0) >= SEVERITY_MAP[:warn]
|
|
568
|
+
@error_logger << "#{line}\n"
|
|
569
|
+
end
|
|
366
570
|
rescue IOError, SystemCallError => e
|
|
367
571
|
raise if @strict
|
|
368
572
|
# Don't crash on log write failure
|
data/lib/tina4/mcp.rb
CHANGED
|
@@ -199,10 +199,12 @@ module Tina4
|
|
|
199
199
|
truthy?(ENV["TINA4_MCP_REMOTE"]) && has_valid_token
|
|
200
200
|
end
|
|
201
201
|
|
|
202
|
-
#
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
202
|
+
# `truthy?` above is Tina4.truthy? (lib/tina4.rb) -> Tina4::Env.is_truthy.
|
|
203
|
+
# This file used to define its OWN Tina4.truthy? here. Both defined the same
|
|
204
|
+
# method on the same module, and because mcp.rb is required BEFORE the
|
|
205
|
+
# definition in tina4.rb, this one was silently overwritten - dead code
|
|
206
|
+
# sitting on the MCP remote-access gate. It only ever looked correct because
|
|
207
|
+
# the two copies happened to hold the same table.
|
|
206
208
|
|
|
207
209
|
# Resolve the dedicated MCP port. Defaults to (server port + 2000) — keeps
|
|
208
210
|
# MCP tooling reachable on a stable, predictable channel separate from the
|
|
@@ -1036,7 +1038,11 @@ module Tina4
|
|
|
1036
1038
|
|
|
1037
1039
|
# ── Session/Cache Tools ───────────────────────────
|
|
1038
1040
|
server.register_tool("session_list", lambda {
|
|
1039
|
-
|
|
1041
|
+
# Same resolution FileHandler uses, so this tool lists the directory
|
|
1042
|
+
# sessions are really in. It hard-coded "data/sessions" while
|
|
1043
|
+
# FileHandler wrote to <cwd>/sessions, so it listed the wrong place on
|
|
1044
|
+
# every project; now both read TINA4_SESSION_PATH with the same default.
|
|
1045
|
+
session_dir = ENV["TINA4_SESSION_PATH"] || File.join("data", "sessions")
|
|
1040
1046
|
return [] unless File.directory?(session_dir)
|
|
1041
1047
|
Dir.glob(File.join(session_dir, "*.json")).map do |f|
|
|
1042
1048
|
begin
|