otto 2.0.0.pre3 → 2.0.0.pre7

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.
Files changed (103) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/.github/workflows/claude-code-review.yml +1 -1
  4. data/.github/workflows/code-smells.yml +146 -0
  5. data/.gitignore +4 -0
  6. data/.pre-commit-config.yaml +2 -2
  7. data/.reek.yml +99 -0
  8. data/CHANGELOG.rst +90 -0
  9. data/CLAUDE.md +74 -540
  10. data/Gemfile +4 -2
  11. data/Gemfile.lock +58 -19
  12. data/README.md +49 -1
  13. data/changelog.d/20251103_235431_delano_86_improve_error_logging.rst +15 -0
  14. data/changelog.d/20251109_025012_claude_fix_backtrace_sanitization.rst +37 -0
  15. data/examples/advanced_routes/README.md +137 -20
  16. data/examples/authentication_strategies/README.md +212 -19
  17. data/examples/backtrace_sanitization_demo.rb +86 -0
  18. data/examples/basic/README.md +61 -10
  19. data/examples/error_handler_registration.rb +136 -0
  20. data/examples/logging_improvements.rb +76 -0
  21. data/examples/mcp_demo/README.md +187 -27
  22. data/examples/security_features/README.md +249 -30
  23. data/examples/simple_geo_resolver.rb +107 -0
  24. data/lib/otto/core/configuration.rb +15 -20
  25. data/lib/otto/core/error_handler.rb +138 -8
  26. data/lib/otto/core/file_safety.rb +2 -2
  27. data/lib/otto/core/freezable.rb +2 -2
  28. data/lib/otto/core/middleware_stack.rb +2 -2
  29. data/lib/otto/core/router.rb +61 -8
  30. data/lib/otto/core/uri_generator.rb +2 -2
  31. data/lib/otto/core.rb +2 -0
  32. data/lib/otto/design_system.rb +2 -2
  33. data/lib/otto/env_keys.rb +61 -12
  34. data/lib/otto/helpers/base.rb +2 -2
  35. data/lib/otto/helpers/request.rb +8 -3
  36. data/lib/otto/helpers/response.rb +2 -2
  37. data/lib/otto/helpers/validation.rb +2 -2
  38. data/lib/otto/helpers.rb +2 -0
  39. data/lib/otto/locale/config.rb +2 -2
  40. data/lib/otto/locale/middleware.rb +160 -0
  41. data/lib/otto/locale.rb +10 -0
  42. data/lib/otto/logging_helpers.rb +273 -0
  43. data/lib/otto/mcp/auth/token.rb +2 -2
  44. data/lib/otto/mcp/protocol.rb +2 -2
  45. data/lib/otto/mcp/rate_limiting.rb +2 -2
  46. data/lib/otto/mcp/registry.rb +2 -2
  47. data/lib/otto/mcp/route_parser.rb +2 -2
  48. data/lib/otto/mcp/schema_validation.rb +2 -2
  49. data/lib/otto/mcp/server.rb +2 -2
  50. data/lib/otto/mcp.rb +2 -0
  51. data/lib/otto/privacy/config.rb +2 -0
  52. data/lib/otto/privacy/geo_resolver.rb +199 -29
  53. data/lib/otto/privacy/ip_privacy.rb +2 -0
  54. data/lib/otto/privacy/redacted_fingerprint.rb +18 -8
  55. data/lib/otto/privacy.rb +2 -0
  56. data/lib/otto/response_handlers/auto.rb +2 -0
  57. data/lib/otto/response_handlers/base.rb +2 -0
  58. data/lib/otto/response_handlers/default.rb +2 -0
  59. data/lib/otto/response_handlers/factory.rb +2 -0
  60. data/lib/otto/response_handlers/json.rb +2 -0
  61. data/lib/otto/response_handlers/redirect.rb +2 -0
  62. data/lib/otto/response_handlers/view.rb +2 -0
  63. data/lib/otto/response_handlers.rb +2 -2
  64. data/lib/otto/route.rb +4 -4
  65. data/lib/otto/route_definition.rb +42 -15
  66. data/lib/otto/route_handlers/base.rb +2 -0
  67. data/lib/otto/route_handlers/class_method.rb +18 -25
  68. data/lib/otto/route_handlers/factory.rb +2 -2
  69. data/lib/otto/route_handlers/instance_method.rb +8 -5
  70. data/lib/otto/route_handlers/lambda.rb +8 -20
  71. data/lib/otto/route_handlers/logic_class.rb +23 -6
  72. data/lib/otto/route_handlers.rb +2 -2
  73. data/lib/otto/security/authentication/auth_failure.rb +2 -2
  74. data/lib/otto/security/authentication/auth_strategy.rb +11 -4
  75. data/lib/otto/security/authentication/route_auth_wrapper.rb +230 -78
  76. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +2 -0
  77. data/lib/otto/security/authentication/strategies/noauth_strategy.rb +2 -0
  78. data/lib/otto/security/authentication/strategies/permission_strategy.rb +2 -0
  79. data/lib/otto/security/authentication/strategies/role_strategy.rb +2 -0
  80. data/lib/otto/security/authentication/strategies/session_strategy.rb +2 -0
  81. data/lib/otto/security/authentication/strategy_result.rb +6 -5
  82. data/lib/otto/security/authentication.rb +2 -2
  83. data/lib/otto/security/authorization_error.rb +73 -0
  84. data/lib/otto/security/config.rb +2 -2
  85. data/lib/otto/security/configurator.rb +17 -2
  86. data/lib/otto/security/csrf.rb +2 -2
  87. data/lib/otto/security/middleware/csrf_middleware.rb +11 -1
  88. data/lib/otto/security/middleware/ip_privacy_middleware.rb +31 -11
  89. data/lib/otto/security/middleware/rate_limit_middleware.rb +2 -0
  90. data/lib/otto/security/middleware/validation_middleware.rb +15 -0
  91. data/lib/otto/security/rate_limiter.rb +2 -2
  92. data/lib/otto/security/rate_limiting.rb +2 -2
  93. data/lib/otto/security/validator.rb +2 -2
  94. data/lib/otto/security.rb +3 -0
  95. data/lib/otto/static.rb +2 -2
  96. data/lib/otto/utils.rb +27 -2
  97. data/lib/otto/version.rb +3 -3
  98. data/lib/otto.rb +174 -14
  99. data/otto.gemspec +7 -3
  100. metadata +24 -15
  101. data/benchmark_middleware_wrap.rb +0 -163
  102. data/changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst +0 -36
  103. data/changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst +0 -5
@@ -0,0 +1,160 @@
1
+ # lib/otto/locale/middleware.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ class Otto
6
+ module Locale
7
+ # Locale detection and resolution middleware
8
+ #
9
+ # Sets env['otto.locale'] based on:
10
+ # 1. URL parameter (?locale=es)
11
+ # 2. Session preference (session['locale'])
12
+ # 3. HTTP Accept-Language header
13
+ # 4. Default locale
14
+ #
15
+ # Configuration:
16
+ # use Otto::Locale::Middleware,
17
+ # available_locales: { 'en' => 'English', 'es' => 'Spanish' },
18
+ # default_locale: 'en',
19
+ # debug: false
20
+ #
21
+ # @example Basic usage
22
+ # use Otto::Locale::Middleware,
23
+ # available_locales: { 'en' => 'English', 'es' => 'Español', 'fr' => 'Français' },
24
+ # default_locale: 'en'
25
+ #
26
+ # @example With session persistence
27
+ # use Rack::Session::Cookie, secret: 'secret'
28
+ # use Otto::Locale::Middleware,
29
+ # available_locales: { 'en' => 'English', 'es' => 'Español' },
30
+ # default_locale: 'en'
31
+ #
32
+ class Middleware
33
+ attr_reader :available_locales, :default_locale
34
+
35
+ # Initialize locale middleware
36
+ #
37
+ # @param app [#call] Rack application
38
+ # @param available_locales [Hash<String, String>] Hash of locale codes to language names
39
+ # @param default_locale [String] Default locale code
40
+ # @param debug [Boolean] Enable debug logging
41
+ def initialize(app, available_locales:, default_locale:, debug: false)
42
+ @app = app
43
+ @available_locales = available_locales
44
+ @default_locale = default_locale
45
+ @debug = debug
46
+
47
+ validate_config!
48
+ end
49
+
50
+ # Process request and set locale
51
+ #
52
+ # @param env [Hash] Rack environment
53
+ # @return [Array] Rack response tuple [status, headers, body]
54
+ def call(env)
55
+ locale = detect_locale(env)
56
+ env['otto.locale'] = locale
57
+
58
+ debug_log(env, locale) if @debug
59
+
60
+ @app.call(env)
61
+ end
62
+
63
+ private
64
+
65
+ # Detect locale using priority chain
66
+ #
67
+ # @param env [Hash] Rack environment
68
+ # @return [String] Resolved locale code
69
+ def detect_locale(env)
70
+ # 1. Check URL parameter
71
+ req = Rack::Request.new(env)
72
+ locale = req.params['locale']
73
+ return locale if valid_locale?(locale)
74
+
75
+ # 2. Check session
76
+ session = env['rack.session']
77
+ locale = session['locale'] if session
78
+ return locale if valid_locale?(locale)
79
+
80
+ # 3. Parse Accept-Language header
81
+ locale = parse_accept_language(env['HTTP_ACCEPT_LANGUAGE'])
82
+ return locale if valid_locale?(locale)
83
+
84
+ # 4. Default
85
+ @default_locale
86
+ end
87
+
88
+ # Parse Accept-Language header with RFC 2616 quality value support
89
+ #
90
+ # Handles formats like:
91
+ # - "en-US,en;q=0.9,fr;q=0.8" → finds first available from [en, en, fr]
92
+ # - "es,en;q=0.9" → returns "en" if "es" unavailable but "en" is
93
+ # - "fr-CA" → "fr"
94
+ #
95
+ # Respects q-values (quality factors) and returns the highest-priority
96
+ # available locale instead of just the first language tag.
97
+ #
98
+ # @param header [String, nil] Accept-Language header value
99
+ # @return [String, nil] Best matching available locale code or nil
100
+ def parse_accept_language(header)
101
+ return nil unless header
102
+
103
+ # Parse all language tags with their q-values
104
+ # Format: "en-US,en;q=0.9,fr;q=0.8" → [[en-US, 1.0], [en, 0.9], [fr, 0.8]]
105
+ languages = header.split(',').map do |tag|
106
+ # Split on semicolon and extract q-value
107
+ parts = tag.strip.split(/\s*;\s*q\s*=\s*/)
108
+ locale_str = parts[0]
109
+ q_value = parts[1] ? parts[1].to_f : 1.0
110
+ [locale_str, q_value]
111
+ end
112
+
113
+ # Sort by q-value descending (highest preference first)
114
+ # and find the first locale that matches available_locales
115
+ languages.sort_by { |_, q| -q }.each do |lang_tag, _|
116
+ # Extract primary language code: "en-US" → "en", "fr" → "fr"
117
+ locale_code = lang_tag.split('-').first.downcase
118
+ return locale_code if valid_locale?(locale_code)
119
+ end
120
+
121
+ nil # No matching locale found
122
+ rescue StandardError => ex
123
+ Otto.logger&.warn "[Otto::Locale] Failed to parse Accept-Language: #{ex.message}"
124
+ nil
125
+ end
126
+
127
+ # Check if locale is valid
128
+ #
129
+ # @param locale [String, nil] Locale code to validate
130
+ # @return [Boolean] true if locale is in available_locales
131
+ def valid_locale?(locale)
132
+ return false unless locale
133
+ @available_locales.key?(locale.to_s)
134
+ end
135
+
136
+ # Validate middleware configuration
137
+ #
138
+ # @raise [ArgumentError] if configuration is invalid
139
+ def validate_config!
140
+ raise ArgumentError, 'available_locales must be a Hash' unless @available_locales.is_a?(Hash)
141
+ raise ArgumentError, 'available_locales cannot be empty' if @available_locales.empty?
142
+ raise ArgumentError, 'default_locale must be in available_locales' unless @available_locales.key?(@default_locale)
143
+ end
144
+
145
+ # Log debug information about locale detection
146
+ #
147
+ # @param env [Hash] Rack environment
148
+ # @param locale [String] Resolved locale
149
+ def debug_log(env, locale)
150
+ Otto.logger&.debug format(
151
+ '[Otto::Locale] Selected locale=%s (param=%s session=%s header=%s)',
152
+ locale,
153
+ Rack::Request.new(env).params['locale'] || 'nil',
154
+ env['rack.session']&.dig('locale') || 'nil',
155
+ env['HTTP_ACCEPT_LANGUAGE']&.split(',')&.first || 'nil'
156
+ )
157
+ end
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,10 @@
1
+ # lib/otto/locale.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ class Otto
6
+ module Locale
7
+ autoload :Config, 'otto/locale/config'
8
+ autoload :Middleware, 'otto/locale/middleware'
9
+ end
10
+ end
@@ -0,0 +1,273 @@
1
+ # lib/otto/logging_helpers.rb
2
+ #
3
+ # frozen_string_literal: true
4
+
5
+ class Otto
6
+ # LoggingHelpers provides utility methods for consistent structured logging
7
+ # across the Otto framework. Centralizes common request context extraction
8
+ # to eliminate duplication while keeping logging calls simple and explicit.
9
+ module LoggingHelpers
10
+ # Structured logging helpers for Otto framework.
11
+ #
12
+ # BASE CONTEXT PATTERN (recommended for downstream projects):
13
+ #
14
+ # Create base context once per error/event, then merge event-specific fields.
15
+ #
16
+ # THREAD SAFETY: This pattern is thread-safe for concurrent requests. Each
17
+ # request has its own `env` hash, so `request_context(env)` creates isolated
18
+ # context hashes per request. The pattern extracts immutable values (strings,
19
+ # symbols) from `env`, and `.merge()` creates new hashes rather than mutating
20
+ # shared state. Safe for use in multi-threaded Rack servers (Puma, Falcon).
21
+ #
22
+ # base_context = Otto::LoggingHelpers.request_context(env)
23
+ #
24
+ # Otto.structured_log(:error, "Handler failed",
25
+ # base_context.merge(
26
+ # error: error.message,
27
+ # error_class: error.class.name,
28
+ # error_id: error_id,
29
+ # duration: duration
30
+ # )
31
+ # )
32
+ #
33
+ # Otto::LoggingHelpers.log_backtrace(error,
34
+ # base_context.merge(error_id: error_id, handler: 'Controller#action')
35
+ # )
36
+ #
37
+ # DOWNSTREAM EXTENSIBILITY:
38
+ #
39
+ # Large projects can inject custom shared fields:
40
+ #
41
+ # custom_base = Otto::LoggingHelpers.request_context(env).merge(
42
+ # transaction_id: Thread.current[:transaction_id],
43
+ # user_id: env['otto.user']&.id,
44
+ # tenant_id: env['tenant_id']
45
+ # )
46
+ #
47
+ # Otto.structured_log(:error, "Business operation failed",
48
+ # custom_base.merge(
49
+ # error: error.message,
50
+ # error_class: error.class.name,
51
+ # account_id: account.id,
52
+ # operation: :withdrawal
53
+ # )
54
+ # )
55
+ #
56
+
57
+ # Extract common request context for structured logging
58
+ #
59
+ # Returns a hash containing privacy-aware request metadata suitable
60
+ # for merging with event-specific data in Otto.structured_log calls.
61
+ #
62
+ # @param env [Hash] Rack environment hash
63
+ # @return [Hash] Request context with method, path, ip, country, user_agent
64
+ #
65
+ # @example Basic usage
66
+ # Otto.structured_log(:info, "Route matched",
67
+ # Otto::LoggingHelpers.request_context(env).merge(
68
+ # type: 'literal',
69
+ # handler: 'App#index'
70
+ # )
71
+ # )
72
+ #
73
+ # @note IP addresses are already masked by IPPrivacyMiddleware (public IPs only)
74
+ # @note User agents are truncated to 100 chars to prevent log bloat
75
+ def self.request_context(env)
76
+ {
77
+ method: env['REQUEST_METHOD'],
78
+ path: env['PATH_INFO'],
79
+ ip: env['REMOTE_ADDR'], # Already masked by IPPrivacyMiddleware for public IPs
80
+ country: env['otto.geo_country'],
81
+ user_agent: env['HTTP_USER_AGENT']&.slice(0, 100), # Already anonymized by IPPrivacyMiddleware
82
+ }.compact
83
+ end
84
+
85
+ # Log a timed operation with consistent timing and error handling
86
+ #
87
+ # @param level [Symbol] The log level (:debug, :info, :warn, :error)
88
+ # @param message [String] The log message
89
+ # @param env [Hash] Rack environment for request context
90
+ # @param metadata [Hash] Additional metadata to include in the log
91
+ # @yield The block to execute and time
92
+ # @return The result of the block
93
+ #
94
+ # @example
95
+ # Otto::LoggingHelpers.log_timed_operation(:info, "Template compiled", env,
96
+ # template_type: 'handlebars', cached: false
97
+ # ) do
98
+ # compile_template(template)
99
+ # end
100
+ #
101
+ def self.log_timed_operation(level, message, env, **metadata)
102
+ start_time = Otto::Utils.now_in_μs
103
+ result = yield
104
+ duration = Otto::Utils.now_in_μs - start_time
105
+
106
+ Otto.structured_log(level, message,
107
+ request_context(env).merge(metadata).merge(duration: duration))
108
+
109
+ result
110
+ rescue StandardError => e
111
+ duration = Otto::Utils.now_in_μs - start_time
112
+ Otto.structured_log(:error, "#{message} failed",
113
+ request_context(env).merge(metadata).merge(
114
+ duration: duration,
115
+ error: e.message,
116
+ error_class: e.class.name
117
+ ))
118
+ raise
119
+ end
120
+
121
+ # Detect project root directory for path sanitization
122
+ # @return [String] Absolute path to project root
123
+ def self.detect_project_root
124
+ @project_root ||= begin
125
+ if defined?(Bundler)
126
+ Bundler.root.to_s
127
+ else
128
+ Dir.pwd
129
+ end
130
+ end
131
+ end
132
+
133
+ # Sanitize a single backtrace line to remove sensitive path information
134
+ #
135
+ # Transforms absolute paths into relative or categorized paths:
136
+ # - Project files: relative path from project root
137
+ # - Gem files: [GEM] gem-name-version/relative/path
138
+ # - Ruby stdlib: [RUBY] filename
139
+ # - Unknown: [EXTERNAL] filename
140
+ #
141
+ # @param line [String] Raw backtrace line
142
+ # @param project_root [String] Project root path (auto-detected if nil)
143
+ # @return [String] Sanitized backtrace line
144
+ #
145
+ # @example
146
+ # sanitize_backtrace_line("/Users/admin/app/lib/user.rb:42:in `save'")
147
+ # # => "lib/user.rb:42:in `save'"
148
+ #
149
+ # sanitize_backtrace_line("/usr/local/gems/rack-3.1.8/lib/rack.rb:10")
150
+ # # => "[GEM] rack-3.1.8/lib/rack.rb:10"
151
+ #
152
+ def self.sanitize_backtrace_line(line, project_root = nil)
153
+ return line if line.nil? || line.empty?
154
+
155
+ project_root ||= detect_project_root
156
+ expanded_root = File.expand_path(project_root)
157
+
158
+ # Extract file path from backtrace line (format: "path:line:in `method'" or "path:line")
159
+ if line =~ /^(.+?):\d+(?::in `.+')?$/
160
+ file_path = ::Regexp.last_match(1)
161
+ suffix = line[file_path.length..]
162
+
163
+ begin
164
+ expanded_path = File.expand_path(file_path)
165
+ rescue ArgumentError
166
+ # Handle malformed paths (e.g., containing null bytes)
167
+ # File.basename also raises ArgumentError for null bytes, so use simple string manipulation
168
+ basename = file_path.split('/').last || file_path
169
+ return "[EXTERNAL] #{basename}#{suffix}"
170
+ end
171
+
172
+ # Try project-relative path first
173
+ if expanded_path.start_with?(expanded_root + File::SEPARATOR)
174
+ relative_path = expanded_path.delete_prefix(expanded_root + File::SEPARATOR)
175
+ return relative_path + suffix
176
+ end
177
+
178
+ # Check for bundler gems specifically (e.g., /bundler/gems/otto-abc123/lib/...)
179
+ # Must check BEFORE regular gems pattern to handle /gems/3.4.0/bundler/gems/...
180
+ if expanded_path =~ %r{/bundler/gems/([^/]+)/(.+)$}
181
+ gem_name = ::Regexp.last_match(1)
182
+ gem_relative = ::Regexp.last_match(2)
183
+ # Strip git hash suffix for cleaner output (otto-abc123def456 → otto)
184
+ gem_name = gem_name.sub(/-[a-f0-9]{7,}$/, '') if gem_name =~ /-[a-f0-9]{7,}$/i
185
+ return "[GEM] #{gem_name}/#{gem_relative}#{suffix}"
186
+ end
187
+
188
+ # Check for regular gem path (e.g., /path/to/gems/rack-3.1.8/lib/rack.rb)
189
+ # Handle version directories: /gems/3.4.0/gems/rack-3.1.8/... by looking for last /gems/
190
+ if expanded_path =~ %r{/gems/([^/]+)/(.+)$}
191
+ gem_name = ::Regexp.last_match(1)
192
+ gem_relative = ::Regexp.last_match(2)
193
+
194
+ # Skip version-only directory names (e.g., 3.4.0)
195
+ # Look deeper if gem_name is just a version number
196
+ if gem_name =~ /^[\d.]+$/ && gem_relative =~ %r{^(?:bundler/)?gems/([^/]+)/(.+)$}
197
+ # Found nested gem path, use that instead
198
+ gem_name = ::Regexp.last_match(1)
199
+ gem_relative = ::Regexp.last_match(2)
200
+ end
201
+
202
+ # Strip version suffix for cleaner output (rack-3.1.8 → rack)
203
+ base_gem_name = gem_name.split('-')[0..-2].join('-')
204
+ base_gem_name = gem_name if base_gem_name.empty?
205
+
206
+ return "[GEM] #{base_gem_name}/#{gem_relative}#{suffix}"
207
+ end
208
+
209
+ # Check for Ruby stdlib (e.g., /path/to/ruby/3.4.0/logger.rb)
210
+ if expanded_path =~ %r{/ruby/[\d.]+/(.+)$}
211
+ stdlib_file = ::Regexp.last_match(1)
212
+ return "[RUBY] #{stdlib_file}#{suffix}"
213
+ end
214
+
215
+ # Unknown/external path - show filename only
216
+ filename = File.basename(file_path)
217
+ return "[EXTERNAL] #{filename}#{suffix}"
218
+ end
219
+
220
+ # Couldn't parse - return as-is (better than failing)
221
+ line
222
+ end
223
+
224
+ # Sanitize an array of backtrace lines
225
+ #
226
+ # @param backtrace [Array<String>] Raw backtrace lines
227
+ # @param project_root [String] Project root path (auto-detected if nil)
228
+ # @return [Array<String>] Sanitized backtrace lines
229
+ def self.sanitize_backtrace(backtrace, project_root: nil)
230
+ return [] if backtrace.nil? || backtrace.empty?
231
+
232
+ project_root ||= detect_project_root
233
+ backtrace.map { |line| sanitize_backtrace_line(line, project_root) }
234
+ end
235
+
236
+ # Log exception backtrace with correlation fields for debugging.
237
+ # Always logs for unhandled errors at ERROR level with sanitized paths.
238
+ # Limits to first 20 lines for critical errors.
239
+ #
240
+ # SECURITY: Paths are sanitized to prevent exposing sensitive system information:
241
+ # - Project files: Show relative paths only
242
+ # - Gem files: Show gem name and relative path within gem
243
+ # - Ruby stdlib: Show filename only
244
+ # - External files: Show filename only
245
+ #
246
+ # Expects caller to provide correlation context (error_id, handler, etc).
247
+ # Does NOT duplicate error/error_class fields - those belong in main error log.
248
+ #
249
+ # @param error [Exception] The exception to log backtrace for
250
+ # @param context [Hash] Correlation fields (error_id, method, path, ip, handler, etc)
251
+ #
252
+ # @example Basic usage
253
+ # Otto::LoggingHelpers.log_backtrace(error,
254
+ # base_context.merge(error_id: error_id, handler: 'UserController#create')
255
+ # )
256
+ #
257
+ # @example Downstream extensibility
258
+ # custom_context = Otto::LoggingHelpers.request_context(env).merge(
259
+ # error_id: error_id,
260
+ # transaction_id: Thread.current[:transaction_id],
261
+ # tenant_id: env['tenant_id']
262
+ # )
263
+ # Otto::LoggingHelpers.log_backtrace(error, custom_context)
264
+ #
265
+ def self.log_backtrace(error, context = {})
266
+ raw_backtrace = error.backtrace&.first(20) || []
267
+ sanitized = sanitize_backtrace(raw_backtrace)
268
+
269
+ Otto.structured_log(:error, 'Exception backtrace',
270
+ context.merge(backtrace: sanitized))
271
+ end
272
+ end
273
+ end
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/mcp/auth/token.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require 'json'
6
6
 
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/mcp/protocol.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require 'json'
6
6
  require_relative 'registry'
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/mcp/rate_limiting.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require 'json'
6
6
 
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/mcp/registry.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  class Otto
6
6
  module MCP
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/mcp/route_parser.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  class Otto
6
6
  module MCP
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/mcp/schema_validation.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require 'json'
6
6
 
@@ -1,6 +1,6 @@
1
- # frozen_string_literal: true
2
-
3
1
  # lib/otto/mcp/server.rb
2
+ #
3
+ # frozen_string_literal: true
4
4
 
5
5
  require_relative 'protocol'
6
6
  require_relative 'registry'
data/lib/otto/mcp.rb CHANGED
@@ -1,3 +1,5 @@
1
1
  # lib/otto/mcp.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require_relative 'mcp/server'
@@ -1,4 +1,6 @@
1
1
  # lib/otto/privacy/config.rb
2
+ #
3
+ # frozen_string_literal: true
2
4
 
3
5
  require 'ipaddr'
4
6
  require 'securerandom'