otto 2.0.0.pre2 → 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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -3
  3. data/.github/workflows/claude-code-review.yml +29 -13
  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 +116 -45
  10. data/Gemfile +5 -2
  11. data/Gemfile.lock +70 -24
  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/docs/.gitignore +1 -0
  16. data/docs/ipaddr-encoding-quirk.md +34 -0
  17. data/docs/migrating/v2.0.0-pre2.md +11 -18
  18. data/examples/advanced_routes/README.md +137 -20
  19. data/examples/authentication_strategies/README.md +212 -19
  20. data/examples/authentication_strategies/config.ru +0 -1
  21. data/examples/backtrace_sanitization_demo.rb +86 -0
  22. data/examples/basic/README.md +61 -10
  23. data/examples/error_handler_registration.rb +136 -0
  24. data/examples/logging_improvements.rb +76 -0
  25. data/examples/mcp_demo/README.md +187 -27
  26. data/examples/security_features/README.md +249 -30
  27. data/examples/simple_geo_resolver.rb +107 -0
  28. data/lib/otto/core/configuration.rb +90 -45
  29. data/lib/otto/core/error_handler.rb +138 -8
  30. data/lib/otto/core/file_safety.rb +2 -2
  31. data/lib/otto/core/freezable.rb +93 -0
  32. data/lib/otto/core/middleware_stack.rb +25 -18
  33. data/lib/otto/core/router.rb +62 -9
  34. data/lib/otto/core/uri_generator.rb +2 -2
  35. data/lib/otto/core.rb +10 -0
  36. data/lib/otto/design_system.rb +2 -2
  37. data/lib/otto/env_keys.rb +65 -12
  38. data/lib/otto/helpers/base.rb +2 -2
  39. data/lib/otto/helpers/request.rb +85 -2
  40. data/lib/otto/helpers/response.rb +5 -5
  41. data/lib/otto/helpers/validation.rb +2 -2
  42. data/lib/otto/helpers.rb +6 -0
  43. data/lib/otto/locale/config.rb +56 -0
  44. data/lib/otto/locale/middleware.rb +160 -0
  45. data/lib/otto/locale.rb +10 -0
  46. data/lib/otto/logging_helpers.rb +273 -0
  47. data/lib/otto/mcp/auth/token.rb +2 -2
  48. data/lib/otto/mcp/protocol.rb +2 -2
  49. data/lib/otto/mcp/rate_limiting.rb +2 -2
  50. data/lib/otto/mcp/registry.rb +2 -2
  51. data/lib/otto/mcp/route_parser.rb +2 -2
  52. data/lib/otto/mcp/schema_validation.rb +2 -2
  53. data/lib/otto/mcp/server.rb +2 -2
  54. data/lib/otto/mcp.rb +5 -0
  55. data/lib/otto/privacy/config.rb +201 -0
  56. data/lib/otto/privacy/geo_resolver.rb +285 -0
  57. data/lib/otto/privacy/ip_privacy.rb +177 -0
  58. data/lib/otto/privacy/redacted_fingerprint.rb +146 -0
  59. data/lib/otto/privacy.rb +31 -0
  60. data/lib/otto/response_handlers/auto.rb +2 -0
  61. data/lib/otto/response_handlers/base.rb +2 -0
  62. data/lib/otto/response_handlers/default.rb +2 -0
  63. data/lib/otto/response_handlers/factory.rb +2 -0
  64. data/lib/otto/response_handlers/json.rb +2 -0
  65. data/lib/otto/response_handlers/redirect.rb +2 -0
  66. data/lib/otto/response_handlers/view.rb +2 -0
  67. data/lib/otto/response_handlers.rb +2 -2
  68. data/lib/otto/route.rb +4 -4
  69. data/lib/otto/route_definition.rb +42 -15
  70. data/lib/otto/route_handlers/base.rb +2 -1
  71. data/lib/otto/route_handlers/class_method.rb +18 -25
  72. data/lib/otto/route_handlers/factory.rb +18 -16
  73. data/lib/otto/route_handlers/instance_method.rb +8 -5
  74. data/lib/otto/route_handlers/lambda.rb +8 -20
  75. data/lib/otto/route_handlers/logic_class.rb +25 -8
  76. data/lib/otto/route_handlers.rb +2 -2
  77. data/lib/otto/security/authentication/{failure_result.rb → auth_failure.rb} +5 -5
  78. data/lib/otto/security/authentication/auth_strategy.rb +13 -6
  79. data/lib/otto/security/authentication/route_auth_wrapper.rb +304 -41
  80. data/lib/otto/security/authentication/strategies/api_key_strategy.rb +2 -0
  81. data/lib/otto/security/authentication/strategies/noauth_strategy.rb +7 -1
  82. data/lib/otto/security/authentication/strategies/permission_strategy.rb +2 -0
  83. data/lib/otto/security/authentication/strategies/role_strategy.rb +2 -0
  84. data/lib/otto/security/authentication/strategies/session_strategy.rb +2 -0
  85. data/lib/otto/security/authentication/strategy_result.rb +6 -5
  86. data/lib/otto/security/authentication.rb +5 -6
  87. data/lib/otto/security/authorization_error.rb +73 -0
  88. data/lib/otto/security/config.rb +53 -9
  89. data/lib/otto/security/configurator.rb +17 -15
  90. data/lib/otto/security/csrf.rb +2 -2
  91. data/lib/otto/security/middleware/csrf_middleware.rb +11 -1
  92. data/lib/otto/security/middleware/ip_privacy_middleware.rb +231 -0
  93. data/lib/otto/security/middleware/rate_limit_middleware.rb +2 -0
  94. data/lib/otto/security/middleware/validation_middleware.rb +15 -0
  95. data/lib/otto/security/rate_limiter.rb +2 -2
  96. data/lib/otto/security/rate_limiting.rb +2 -2
  97. data/lib/otto/security/validator.rb +2 -2
  98. data/lib/otto/security.rb +12 -0
  99. data/lib/otto/static.rb +2 -2
  100. data/lib/otto/utils.rb +27 -2
  101. data/lib/otto/version.rb +3 -3
  102. data/lib/otto.rb +344 -89
  103. data/otto.gemspec +9 -2
  104. metadata +72 -8
  105. data/lib/otto/security/authentication/authentication_middleware.rb +0 -140
@@ -1,32 +1,225 @@
1
1
  # Otto - Authentication Strategies Example
2
2
 
3
- This example demonstrates how to use Otto's powerful authentication features.
3
+ This example demonstrates Otto's flexible authentication system with multiple strategies, token validation, and role-based access control.
4
+
5
+ ## What You'll Learn
6
+
7
+ - How to configure multiple authentication strategies
8
+ - Token-based authentication with session validation
9
+ - API key authentication for programmatic access
10
+ - Role and permission-based access control
11
+ - How to protect routes with authentication requirements
12
+ - Handling authentication failures and redirects
4
13
 
5
14
  ## Structure
6
15
 
7
- * `config.ru`: The Rackup file. It initializes Otto and loads the authentication strategies.
8
- * `routes`: Defines the application's routes and the authentication required for each.
9
- * `app/auth.rb`: Contains the definitions for all authentication strategies. This is where you would add your own.
10
- * `app/controllers/`: Contains the controller classes that handle requests.
16
+ - `config.ru`: Rack configuration that initializes Otto and loads auth strategies
17
+ - `routes`: Application routes with authentication requirements
18
+ - `app/auth.rb`: Authentication strategy definitions and token setup
19
+ - `app/controllers/`: Handler classes for protected and public routes
20
+
21
+ ## Authentication Strategies in This Example
22
+
23
+ ### Token-Based Auth
24
+ Validates user tokens for web applications:
25
+ ```
26
+ GET /profile HomeController#profile auth=token
27
+ ```
28
+ Requires: `?token=demo_token`
29
+
30
+ ### Admin Role Auth
31
+ Validates admin-level access:
32
+ ```
33
+ GET /admin AdminController#dashboard auth=admin
34
+ ```
35
+ Requires: `?token=admin_token`
36
+
37
+ ### Permission-Based Auth
38
+ Validates specific permissions:
39
+ ```
40
+ POST /edit ArticleController#update auth=can_write
41
+ ```
42
+ Requires: `?token=demo_token` (with write permission)
43
+
44
+ ### API Key Auth
45
+ Validates API keys for programmatic access:
46
+ ```
47
+ GET /api/data ApiController#show auth=api_key
48
+ ```
49
+ Requires: `?api_key=demo_api_key_123`
50
+
51
+ ## How to Run
52
+
53
+ ### Using rackup (recommended)
54
+
55
+ ```sh
56
+ cd examples/authentication_strategies
57
+ rackup config.ru
58
+ ```
59
+
60
+ ### Using alternative servers
61
+
62
+ ```sh
63
+ thin -R config.ru -p 9292 start
64
+ puma config.ru -p 9292
65
+ ```
66
+
67
+ Open your browser and navigate to `http://localhost:9292`.
68
+
69
+ ## Testing Authentication
70
+
71
+ ### Web Browser (Token-based)
72
+
73
+ Click these links or visit them directly:
74
+
75
+ - **Public page**: [http://localhost:9292/](http://localhost:9292/)
76
+ - **Authenticated user**: [http://localhost:9292/profile?token=demo_token](http://localhost:9292/profile?token=demo_token)
77
+ - **Admin user**: [http://localhost:9292/admin?token=admin_token](http://localhost:9292/admin?token=admin_token)
78
+ - **User with write permission**: [http://localhost:9292/edit?token=demo_token](http://localhost:9292/edit?token=demo_token)
79
+
80
+ ### curl Commands (API Key)
81
+
82
+ ```sh
83
+ # Without API key (fails)
84
+ curl http://localhost:9292/api/data
85
+
86
+ # With API key (succeeds)
87
+ curl "http://localhost:9292/api/data?api_key=demo_api_key_123"
88
+ ```
89
+
90
+ ### Testing Invalid Credentials
91
+
92
+ Try accessing protected routes without valid credentials:
93
+
94
+ ```sh
95
+ # No token - redirects to login or returns 401
96
+ curl http://localhost:9292/profile
97
+
98
+ # Invalid token - returns 401
99
+ curl "http://localhost:9292/profile?token=invalid"
100
+
101
+ # Wrong token type - returns 401
102
+ curl "http://localhost:9292/admin?api_key=demo_api_key_123"
103
+ ```
104
+
105
+ ## Expected Output
106
+
107
+ ### Successful Authentication
108
+ ```
109
+ HTTP/1.1 200 OK
110
+ Content-Type: text/html
111
+
112
+ <h1>Welcome, alice!</h1>
113
+ <p>This is your profile.</p>
114
+ ```
115
+
116
+ ### Failed Authentication
117
+ ```
118
+ HTTP/1.1 401 Unauthorized
119
+ Content-Type: text/plain
120
+
121
+ Unauthorized
122
+ ```
123
+
124
+ ### Redirect to Login
125
+ ```
126
+ HTTP/1.1 302 Found
127
+ Location: http://localhost:9292/?login=required
128
+ ```
129
+
130
+ ## File Structure Details
131
+
132
+ ### Routes File
133
+ - Public routes (no `auth=` requirement)
134
+ - Protected routes with different auth strategies
135
+ - Admin-only routes
136
+ - API routes with API key authentication
137
+
138
+ ### Auth Strategies (`app/auth.rb`)
139
+ - Token validation logic with demo tokens
140
+ - Admin role checking
141
+ - Permission validation (read, write, admin)
142
+ - API key validation for programmatic access
143
+
144
+ ### Controllers (`app/controllers/`)
145
+ - Welcome controller for public pages
146
+ - Profile controller for authenticated users
147
+ - Admin controller for admin-only pages
148
+ - Article controller for permission-based access
149
+ - API controller for programmatic access
150
+
151
+ ## Key Concepts
152
+
153
+ ### Strategy Registration
154
+ Strategies are registered in `config.ru` before the first request:
155
+
156
+ ```ruby
157
+ app.add_auth_strategy('token', TokenStrategy.new)
158
+ app.add_auth_strategy('admin', AdminStrategy.new)
159
+ app.add_auth_strategy('api_key', APIKeyStrategy.new)
160
+ ```
161
+
162
+ ### Route Protection
163
+ Routes specify their auth requirement in the routes file:
164
+
165
+ ```
166
+ GET /protected Controller#method auth=token
167
+ POST /admin Controller#admin auth=admin
168
+ ```
169
+
170
+ ### User Context
171
+ After successful authentication, `req.user_context` contains user info:
172
+
173
+ ```ruby
174
+ def profile
175
+ user_id = @req.user_context[:user_id]
176
+ @res.body = "Welcome, #{user_id}!"
177
+ end
178
+ ```
179
+
180
+ ## Demo Credentials
181
+
182
+ ### Tokens
183
+ - `demo_token` - Regular user (Alice)
184
+ - Permissions: read, write
185
+ - Roles: user
186
+ - `admin_token` - Administrator
187
+ - Permissions: read, write, admin
188
+ - Roles: admin, user
189
+
190
+ ### API Keys
191
+ - `demo_api_key_123` - Demo API access
192
+ - Additional keys can be added to `app/auth.rb`
193
+
194
+ ## Customizing Authentication
11
195
 
12
- ## Running the Demo
196
+ To add your own authentication:
13
197
 
14
- 1. Make sure you have the necessary gems installed (`bundle install`).
15
- 2. Run the application from the root of the `otto` project:
198
+ 1. **Create a strategy class**:
199
+ ```ruby
200
+ class MyStrategy < Otto::Security::Authentication::AuthStrategy
201
+ def authenticate(env, requirement)
202
+ # Validate credentials
203
+ success_result(user_id: 'alice') # or failure_result
204
+ end
205
+ end
206
+ ```
16
207
 
17
- ```sh
18
- rackup examples/authentication_strategies/config.ru
19
- ```
208
+ 2. **Register it in config.ru**:
209
+ ```ruby
210
+ app.add_auth_strategy('my_strategy', MyStrategy.new)
211
+ ```
20
212
 
21
- 3. Open your browser and navigate to `http://localhost:9292`.
213
+ 3. **Use it in routes**:
214
+ ```
215
+ GET /protected Controller#method auth=my_strategy
216
+ ```
22
217
 
23
- ## Trying the Authentication Strategies
218
+ ## Next Steps
24
219
 
25
- You can test the different authentication strategies by providing a `token` or `api_key` parameter in the URL.
220
+ - Explore [Security Features](../security_features/) for CSRF, input validation, file uploads
221
+ - Review [Advanced Routes](../advanced_routes/) for response types and logic classes
26
222
 
27
- * **Authenticated User:** [http://localhost:9292/profile?token=demo_token](http://localhost:9292/profile?token=demo_token)
28
- * **Admin User:** [http://localhost:9292/admin?token=admin_token](http://localhost:9292/admin?token=admin_token)
29
- * **User with 'write' permission:** [http://localhost:9292/edit?token=demo_token](http://localhost:9292/edit?token=demo_token)
30
- * **API Key:** [http://localhost:9292/api/data?api_key=demo_api_key_123](http://localhost:9292/api/data?api_key=demo_api_key_123)
223
+ ## Further Reading
31
224
 
32
- If you try to access a protected route without the correct token, you'll get an authentication error.
225
+ - [CLAUDE.md](../../CLAUDE.md#authentication-architecture) - Detailed auth documentation
@@ -12,7 +12,6 @@ otto = Otto.new('routes')
12
12
  # Enable security features to demonstrate advanced route parameters
13
13
  otto.enable_csrf_protection!
14
14
  otto.enable_request_validation!
15
- otto.enable_authentication!
16
15
 
17
16
  # Load and configure authentication strategies
18
17
  AuthenticationSetup.configure(otto)
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Demo script showing Otto's automatic backtrace sanitization in structured_log
5
+ #
6
+ # This demonstrates that Otto now automatically sanitizes backtraces when they
7
+ # appear in structured log data, eliminating the need for monkey patches.
8
+
9
+ require_relative '../lib/otto'
10
+ require 'logger'
11
+
12
+ # Set up Otto with a logger to see the output
13
+ Otto.logger = Logger.new($stdout)
14
+ Otto.logger.level = Logger::DEBUG
15
+ Otto.debug = true
16
+
17
+ puts "=== Otto Backtrace Sanitization Demo ==="
18
+ puts
19
+
20
+ # Example 1: Raw backtrace with sensitive paths
21
+ puts "1. Raw backtrace with sensitive system paths:"
22
+ raw_backtrace = [
23
+ '/Users/admin/secret-project/app/controllers/users_controller.rb:42:in `create\'',
24
+ '/home/deploy/.rbenv/versions/3.2.0/lib/ruby/gems/3.2.0/gems/rack-3.1.8/lib/rack/builder.rb:310:in `call\'',
25
+ '/usr/local/ruby/3.2.0/lib/ruby/3.2.0/logger.rb:310:in `add\'',
26
+ '/opt/bundler/gems/custom-gem-abc123def456/lib/custom.rb:50:in `process\'',
27
+ '/some/unknown/external/path/mystery.rb:100:in `mystery_method\''
28
+ ]
29
+
30
+ Otto.structured_log(:error, 'Exception backtrace', {
31
+ error_id: 'demo123',
32
+ error: 'User creation failed',
33
+ backtrace: raw_backtrace
34
+ })
35
+
36
+ puts
37
+
38
+ # Example 2: Non-backtrace data remains unchanged
39
+ puts "2. Non-backtrace arrays are not affected:"
40
+ Otto.structured_log(:info, 'Request processed', {
41
+ method: 'POST',
42
+ path: '/users',
43
+ tags: ['important', 'user-creation', 'api'],
44
+ middleware_stack: ['CSRF', 'Auth', 'RateLimit']
45
+ })
46
+
47
+ puts
48
+
49
+ # Example 3: Mixed data with backtrace
50
+ puts "3. Mixed data with backtrace gets selectively sanitized:"
51
+ Otto.structured_log(:warn, 'Validation warning with context', {
52
+ user_id: 'user_456',
53
+ validation_errors: ['email_invalid', 'password_too_short'],
54
+ backtrace: [
55
+ '/Users/developer/my-app/lib/validators/email.rb:25:in `validate_format\'',
56
+ '/home/app/.bundle/gems/activemodel-7.0.0/lib/active_model/validator.rb:155:in `validate\''
57
+ ],
58
+ request_id: 'req_789'
59
+ })
60
+
61
+ puts
62
+
63
+ # Example 4: Empty or nil backtrace handling
64
+ puts "4. Handles edge cases gracefully:"
65
+ Otto.structured_log(:debug, 'Debug with empty backtrace', {
66
+ event: 'method_entry',
67
+ backtrace: [],
68
+ timestamp: Time.now.to_f
69
+ })
70
+
71
+ Otto.structured_log(:info, 'Info with nil backtrace', {
72
+ event: 'cache_hit',
73
+ backtrace: nil,
74
+ cache_key: 'user:123'
75
+ })
76
+
77
+ puts
78
+ puts "=== Demo Complete ==="
79
+ puts
80
+ puts "Notice how:"
81
+ puts "• Project paths become relative: 'app/controllers/users_controller.rb:42'"
82
+ puts "• Gem paths get [GEM] prefix with versions removed: '[GEM] rack/lib/rack/builder.rb:310'"
83
+ puts "• Ruby stdlib gets [RUBY] prefix: '[RUBY] logger.rb:310'"
84
+ puts "• Unknown paths get [EXTERNAL] prefix: '[EXTERNAL] mystery.rb:100'"
85
+ puts "• Non-backtrace arrays remain unchanged"
86
+ puts "• This happens automatically in Otto.structured_log - no monkey patching needed!"
@@ -2,28 +2,79 @@
2
2
 
3
3
  This example demonstrates a basic Otto application with a single route that accepts both GET and POST requests.
4
4
 
5
+ ## What You'll Learn
6
+
7
+ - How to define routes in plain-text format
8
+ - Creating a basic request handler class
9
+ - Working with Rack request and response objects
10
+ - Running an Otto application with different servers
11
+ - Simple form handling and redirects
12
+
5
13
  ## How to Run
6
14
 
7
- 1. Make sure you have `bundler` and `thin` installed:
15
+ ### Using rackup (recommended)
16
+
8
17
  ```sh
9
- gem install bundler thin
18
+ cd examples/basic
19
+ rackup config.ru -p 10770
10
20
  ```
11
21
 
12
- 2. Install the dependencies from the root of the project:
22
+ ### Using thin
23
+
13
24
  ```sh
14
- bundle install
25
+ cd examples/basic
26
+ thin -e dev -R config.ru -p 10770 start
15
27
  ```
16
28
 
17
- 3. Start the server from this directory (`examples/basic`):
29
+ ### Using puma
30
+
18
31
  ```sh
19
- thin -e dev -R config.ru -p 10770 start
32
+ cd examples/basic
33
+ puma config.ru -p 10770
34
+ ```
35
+
36
+ Open your browser and navigate to `http://localhost:10770`.
37
+
38
+ ## Expected Output
39
+
20
40
  ```
41
+ Puma starting in single threaded mode...
42
+ * Version 3.12.0 (ruby 3.2.0-p0), codename: Llama Litter Box
43
+ * Min threads: 0, max threads: 32
44
+ * Environment: development
45
+ * Listening on tcp://127.0.0.1:10770
21
46
 
22
- 4. Open your browser and navigate to `http://localhost:10770`.
47
+ [GET request to /]
48
+ GET / 200 OK
49
+
50
+ [Submitting feedback form]
51
+ POST /feedback 302 Found
52
+ Location: http://localhost:10770/
53
+ ```
54
+
55
+ Then visit `http://localhost:10770` and submit feedback to see it in action.
23
56
 
24
57
  ## File Structure
25
58
 
26
59
  * `README.md`: This file.
27
- * `app.rb`: Contains the application logic. It has two methods: `index` to display the main page and `receive_feedback` to handle form submissions.
28
- * `config.ru`: The Rack configuration file that loads the Otto framework and the application.
29
- * `routes`: Defines the URL routes and maps them to methods in the `App` class.
60
+ * `app.rb`: Contains the application logic with two methods:
61
+ - `index`: Displays the main page with a feedback form
62
+ - `receive_feedback`: Handles form submissions and redirects back home
63
+ * `config.ru`: The Rack configuration file that loads Otto and the application.
64
+ * `routes`: Defines the URL routes mapping to methods in the `App` class.
65
+
66
+ ## Trying It Out
67
+
68
+ 1. **View the home page**: Open `http://localhost:10770` in your browser
69
+ 2. **Submit feedback**: Enter text in the feedback form and click Submit
70
+ 3. **Check the redirect**: You should be redirected back to the home page
71
+
72
+ ## Next Steps
73
+
74
+ - Explore [Advanced Routes](../advanced_routes/) to learn about response type negotiation
75
+ - Check out [Authentication](../authentication_strategies/) for protecting routes
76
+ - See [Security Features](../security_features/) for CSRF, input validation, and more
77
+
78
+ ## Further Reading
79
+
80
+ - [CLAUDE.md](../../CLAUDE.md) - Developer guidance and patterns
@@ -0,0 +1,136 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example: Error Handler Registration
5
+ #
6
+ # This example demonstrates how to register custom error handlers for expected
7
+ # business logic errors, preventing them from being logged as unhandled 500 errors.
8
+
9
+ require_relative '../lib/otto'
10
+
11
+ # Define some business logic error classes
12
+ module MyApp
13
+ class MissingResourceError < StandardError; end
14
+ class ExpiredResourceError < StandardError; end
15
+
16
+ class RateLimitError < StandardError
17
+ attr_reader :retry_after
18
+
19
+ def initialize(message, retry_after: 60)
20
+ super(message)
21
+ @retry_after = retry_after
22
+ end
23
+ end
24
+ end
25
+
26
+ # Create routes file
27
+ routes_content = <<~ROUTES
28
+ GET /resource/:id ResourceHandler.show
29
+ POST /action ActionHandler.process
30
+ ROUTES
31
+
32
+ File.write('/tmp/otto_error_routes.txt', routes_content)
33
+
34
+ # Define handlers that might raise expected errors
35
+ class ResourceHandler
36
+ def self.show(req, res)
37
+ resource_id = req.params[:id]
38
+
39
+ # Simulate resource lookup
40
+ raise MyApp::MissingResourceError, "Resource #{resource_id} not found" if resource_id == 'missing'
41
+ raise MyApp::ExpiredResourceError, "Resource #{resource_id} expired" if resource_id == 'expired'
42
+
43
+ res.status = 200
44
+ res.headers['Content-Type'] = 'application/json'
45
+ res.write(JSON.generate({ id: resource_id, name: "Resource #{resource_id}" }))
46
+ end
47
+ end
48
+
49
+ class ActionHandler
50
+ def self.process(req, res)
51
+ # Simulate rate limiting
52
+ raise MyApp::RateLimitError.new('Too many requests', retry_after: 120)
53
+ end
54
+ end
55
+
56
+ # Create Otto app
57
+ otto = Otto.new('/tmp/otto_error_routes.txt')
58
+
59
+ # Register error handlers BEFORE first request
60
+ puts "Registering error handlers..."
61
+
62
+ # Basic registration with status code and log level
63
+ otto.register_error_handler(MyApp::MissingResourceError, status: 404, log_level: :info)
64
+ otto.register_error_handler(MyApp::ExpiredResourceError, status: 410, log_level: :info)
65
+
66
+ # Advanced registration with custom response handler
67
+ otto.register_error_handler(MyApp::RateLimitError, status: 429, log_level: :warn) do |error, req|
68
+ {
69
+ error: 'RateLimited',
70
+ message: error.message,
71
+ retry_after: error.retry_after,
72
+ path: req.path
73
+ }
74
+ end
75
+
76
+ puts "\n=== Test 1: Missing Resource (404) ==="
77
+ env = {
78
+ 'REQUEST_METHOD' => 'GET',
79
+ 'PATH_INFO' => '/resource/missing',
80
+ 'HTTP_ACCEPT' => 'application/json',
81
+ 'REMOTE_ADDR' => '127.0.0.1'
82
+ }
83
+
84
+ status, headers, body = otto.call(env)
85
+ puts "Status: #{status}"
86
+ puts "Content-Type: #{headers['content-type']}"
87
+ puts "Body: #{body.first}"
88
+ puts "Log level: INFO (not ERROR)"
89
+
90
+ puts "\n=== Test 2: Expired Resource (410) ==="
91
+ env['PATH_INFO'] = '/resource/expired'
92
+
93
+ status, headers, body = otto.call(env)
94
+ puts "Status: #{status}"
95
+ puts "Content-Type: #{headers['content-type']}"
96
+ puts "Body: #{body.first}"
97
+ puts "Log level: INFO (not ERROR)"
98
+
99
+ puts "\n=== Test 3: Rate Limited (429) with custom handler ==="
100
+ env = {
101
+ 'REQUEST_METHOD' => 'POST',
102
+ 'PATH_INFO' => '/action',
103
+ 'HTTP_ACCEPT' => 'application/json',
104
+ 'REMOTE_ADDR' => '127.0.0.1'
105
+ }
106
+
107
+ status, headers, body = otto.call(env)
108
+ puts "Status: #{status}"
109
+ puts "Content-Type: #{headers['content-type']}"
110
+ puts "Body: #{body.first}"
111
+ puts "Log level: WARN (not ERROR)"
112
+ puts "Custom fields: retry_after included"
113
+
114
+ puts "\n=== Test 4: Successful Request ==="
115
+ env = {
116
+ 'REQUEST_METHOD' => 'GET',
117
+ 'PATH_INFO' => '/resource/123',
118
+ 'HTTP_ACCEPT' => 'application/json',
119
+ 'REMOTE_ADDR' => '127.0.0.1'
120
+ }
121
+
122
+ status, headers, body = otto.call(env)
123
+ puts "Status: #{status}"
124
+ puts "Content-Type: #{headers['content-type']}"
125
+ puts "Body: #{body.first}"
126
+
127
+ puts "\n=== Benefits ==="
128
+ puts "✓ Expected errors return proper HTTP status codes (not 500)"
129
+ puts "✓ Logged at INFO/WARN level (not ERROR)"
130
+ puts "✓ No backtrace spam for expected conditions"
131
+ puts "✓ Still generates error IDs for correlation"
132
+ puts "✓ Custom response handlers for complex error data"
133
+ puts "✓ Content negotiation (JSON/plain text) automatic"
134
+
135
+ # Cleanup
136
+ File.delete('/tmp/otto_error_routes.txt') if File.exist?('/tmp/otto_error_routes.txt')
@@ -0,0 +1,76 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Example demonstrating Otto's enhanced logging capabilities
5
+ # Inspired by structured logging patterns with timing
6
+
7
+ require_relative '../lib/otto'
8
+
9
+ # Set up Otto with structured logging
10
+ Otto.logger = Logger.new(STDOUT)
11
+ Otto.debug = true
12
+
13
+ # Create a simple Otto app
14
+ otto = Otto.new do |routes|
15
+ routes << "GET /example App#handle_request"
16
+ routes << "GET /timed App#timed_operation"
17
+ end
18
+
19
+ # Example handler class demonstrating the new logging patterns
20
+ class App
21
+ def self.handle_request(req, res)
22
+ # Example of structured logging with request context
23
+ Otto.structured_log(:info, "Request processed",
24
+ Otto::LoggingHelpers.request_context(req.env).merge(
25
+ user_id: req.params['user_id'],
26
+ cached: false,
27
+ response_size_bytes: 1024
28
+ )
29
+ )
30
+
31
+ res.write("Hello World")
32
+ res
33
+ end
34
+
35
+ def self.timed_operation(req, res)
36
+ # Example of the log_timed_operation helper
37
+ result = Otto::LoggingHelpers.log_timed_operation(:info, "Template rendered", req.env,
38
+ template: 'example_template',
39
+ layout: 'application',
40
+ partials: ['header', 'footer']
41
+ ) do
42
+ # Simulate some work
43
+ sleep(0.01)
44
+ "Rendered content"
45
+ end
46
+
47
+ # Alternative: Manual timing with structured_log
48
+ Otto.structured_log(:debug, "Cache lookup",
49
+ Otto::LoggingHelpers.request_context(req.env).merge(
50
+ cache_key: 'template:example',
51
+ cache_hit: true,
52
+ cache_ttl: 3600
53
+ )
54
+ )
55
+
56
+ res.write(result)
57
+ res
58
+ end
59
+ end
60
+
61
+ # Test the logging
62
+ puts "\n=== Testing Enhanced Logging ==="
63
+ puts "\n1. Standard request (uses structured_log):"
64
+ env1 = { 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/example', 'QUERY_STRING' => 'user_id=123' }
65
+ otto.call(env1)
66
+
67
+ puts "\n2. Timed operation (uses log_timed_operation):"
68
+ env2 = { 'REQUEST_METHOD' => 'GET', 'PATH_INFO' => '/timed' }
69
+ otto.call(env2)
70
+
71
+ puts "\n=== Expected Output Format ==="
72
+ puts "Standard logger (structured_log):"
73
+ puts "I, [timestamp] INFO -- : [Otto] Request processed -- {method: \"GET\", path: \"/example\", ip: \"127.0.0.1\", user_id: \"123\", cached: false, response_size_bytes: 1024}"
74
+
75
+ puts "\nStructured logging with timing (log_timed_operation):"
76
+ puts "I, [timestamp] INFO -- : [Otto] Template rendered -- {method: \"GET\", path: \"/timed\", ip: \"127.0.0.1\", template: \"example_template\", layout: \"application\", partials: [\"header\", \"footer\"], duration: 10123}"