Module: TalkToYourApp::AuditLogger
- Defined in:
- lib/talk_to_your_app/audit_logger.rb
Overview
Emits exactly one structured log line per tool invocation. Wrapping happens at the tool dispatch boundary, where the principal, params, plugin, tool, and timing are all in scope. Defaults to the configured logger (Rails.logger) at the plugin’s level (default INFO). Re-raises on failure so the SDK still surfaces a tool error to the client.
Class Method Summary collapse
-
.around(tool_class:, plugin_name:, log_level:, params:) ⇒ Object
Runs the block, times it, and logs the outcome.
-
.build_fields(plugin_name, tool_class, params, outcome, error_class, duration_ms) ⇒ Object
The structured audit payload for one tool invocation.
- .default_logger ⇒ Object
- .emit(plugin_name:, tool_class:, params:, outcome:, error_class:, duration_ms:, log_level:) ⇒ Object
- .format_line(fields) ⇒ Object
- .format_value(value) ⇒ Object
- .logger ⇒ Object
- .monotonic ⇒ Object
-
.redact(tool_class, params) ⇒ Object
Replaces any argument declared with ‘redact: true` with [REDACTED].
Class Method Details
.around(tool_class:, plugin_name:, log_level:, params:) ⇒ Object
Runs the block, times it, and logs the outcome. Returns the block’s value.
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/talk_to_your_app/audit_logger.rb', line 17 def around(tool_class:, plugin_name:, log_level:, params:) started = monotonic error_class = nil outcome = "success" begin result = yield outcome = "error" if result.is_a?(MCP::Tool::Response) && result.error? result rescue StandardError => e outcome = "error" error_class = e.class.name raise ensure # A logging failure must never replace the tool's result or its # exception (an exception raised in `ensure` would do exactly that), so # emit defensively and swallow any logger error to $stderr. begin emit( plugin_name: plugin_name, tool_class: tool_class, params: params, outcome: outcome, error_class: error_class, duration_ms: ((monotonic - started) * 1000).round(2), log_level: log_level, ) rescue StandardError => e warn("talk_to_your_app: audit logging failed: #{e.class}: #{e.}") end end end |
.build_fields(plugin_name, tool_class, params, outcome, error_class, duration_ms) ⇒ Object
The structured audit payload for one tool invocation.
62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 |
# File 'lib/talk_to_your_app/audit_logger.rb', line 62 def build_fields(plugin_name, tool_class, params, outcome, error_class, duration_ms) fields = { ts: Time.now.utc.iso8601(3), principal: TalkToYourApp::Current.principal, session_id: TalkToYourApp::Current.session_id, ip: TalkToYourApp::Current.ip, plugin: plugin_name, tool: tool_class.tool_name, params: redact(tool_class, params), outcome: outcome, duration_ms: duration_ms, } fields[:error_class] = error_class if error_class fields end |
.default_logger ⇒ Object
102 103 104 105 106 107 108 109 |
# File 'lib/talk_to_your_app/audit_logger.rb', line 102 def default_logger if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger Rails.logger else require "logger" @fallback_logger ||= Logger.new($stdout) end end |
.emit(plugin_name:, tool_class:, params:, outcome:, error_class:, duration_ms:, log_level:) ⇒ Object
49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/talk_to_your_app/audit_logger.rb', line 49 def emit(plugin_name:, tool_class:, params:, outcome:, error_class:, duration_ms:, log_level:) fields = build_fields(plugin_name, tool_class, params, outcome, error_class, duration_ms) # Structured event for anyone who wants to persist a full audit trail # (e.g. an Activity table with IP + principal). Subscribers receive the # `fields` hash as the payload. See the README "Custom audit logging". ActiveSupport::Notifications.instrument("talk_to_your_app.tool_call", fields) level = (log_level || TalkToYourApp.configuration.log_level || :info) logger.public_send(level) { format_line(fields) } end |
.format_line(fields) ⇒ Object
78 79 80 |
# File 'lib/talk_to_your_app/audit_logger.rb', line 78 def format_line(fields) "talk_to_your_app " + fields.map { |k, v| "#{k}=#{format_value(v)}" }.join(" ") end |
.format_value(value) ⇒ Object
90 91 92 93 94 95 96 |
# File 'lib/talk_to_your_app/audit_logger.rb', line 90 def format_value(value) case value when Hash, Array then value.to_json when nil then "-" else value.to_s end end |
.logger ⇒ Object
98 99 100 |
# File 'lib/talk_to_your_app/audit_logger.rb', line 98 def logger TalkToYourApp.configuration.logger || default_logger end |
.monotonic ⇒ Object
111 112 113 |
# File 'lib/talk_to_your_app/audit_logger.rb', line 111 def monotonic Process.clock_gettime(Process::CLOCK_MONOTONIC) end |
.redact(tool_class, params) ⇒ Object
Replaces any argument declared with ‘redact: true` with [REDACTED].
83 84 85 86 87 88 |
# File 'lib/talk_to_your_app/audit_logger.rb', line 83 def redact(tool_class, params) params.each_with_object({}) do |(key, value), acc| opts = tool_class.arguments[key.to_sym] acc[key] = opts && opts[:redact] ? "[REDACTED]" : value end end |