otto 2.0.0.pre3 → 2.0.0.pre8
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/.github/workflows/ci.yml +1 -1
- data/.github/workflows/claude-code-review.yml +1 -1
- data/.github/workflows/code-smells.yml +143 -0
- data/.gitignore +4 -0
- data/.pre-commit-config.yaml +2 -2
- data/.reek.yml +99 -0
- data/CHANGELOG.rst +156 -0
- data/CLAUDE.md +74 -540
- data/Gemfile +4 -2
- data/Gemfile.lock +58 -19
- data/README.md +49 -1
- data/examples/advanced_routes/README.md +137 -20
- data/examples/authentication_strategies/README.md +212 -19
- data/examples/backtrace_sanitization_demo.rb +86 -0
- data/examples/basic/README.md +61 -10
- data/examples/error_handler_registration.rb +136 -0
- data/examples/logging_improvements.rb +76 -0
- data/examples/mcp_demo/README.md +187 -27
- data/examples/security_features/README.md +249 -30
- data/examples/simple_geo_resolver.rb +107 -0
- data/lib/otto/core/configuration.rb +15 -20
- data/lib/otto/core/error_handler.rb +138 -8
- data/lib/otto/core/file_safety.rb +2 -2
- data/lib/otto/core/freezable.rb +2 -2
- data/lib/otto/core/middleware_stack.rb +2 -2
- data/lib/otto/core/router.rb +61 -8
- data/lib/otto/core/uri_generator.rb +2 -2
- data/lib/otto/core.rb +2 -0
- data/lib/otto/design_system.rb +2 -2
- data/lib/otto/env_keys.rb +61 -12
- data/lib/otto/helpers/base.rb +2 -2
- data/lib/otto/helpers/request.rb +8 -3
- data/lib/otto/helpers/response.rb +2 -2
- data/lib/otto/helpers/validation.rb +2 -2
- data/lib/otto/helpers.rb +2 -0
- data/lib/otto/locale/config.rb +2 -2
- data/lib/otto/locale/middleware.rb +160 -0
- data/lib/otto/locale.rb +10 -0
- data/lib/otto/logging_helpers.rb +273 -0
- data/lib/otto/mcp/auth/token.rb +2 -2
- data/lib/otto/mcp/protocol.rb +2 -2
- data/lib/otto/mcp/rate_limiting.rb +2 -2
- data/lib/otto/mcp/registry.rb +2 -2
- data/lib/otto/mcp/route_parser.rb +2 -2
- data/lib/otto/mcp/schema_validation.rb +2 -2
- data/lib/otto/mcp/server.rb +2 -2
- data/lib/otto/mcp.rb +2 -0
- data/lib/otto/privacy/config.rb +2 -0
- data/lib/otto/privacy/geo_resolver.rb +199 -29
- data/lib/otto/privacy/ip_privacy.rb +2 -0
- data/lib/otto/privacy/redacted_fingerprint.rb +18 -8
- data/lib/otto/privacy.rb +2 -0
- data/lib/otto/response_handlers/auto.rb +2 -0
- data/lib/otto/response_handlers/base.rb +2 -0
- data/lib/otto/response_handlers/default.rb +2 -0
- data/lib/otto/response_handlers/factory.rb +2 -0
- data/lib/otto/response_handlers/json.rb +2 -0
- data/lib/otto/response_handlers/redirect.rb +2 -0
- data/lib/otto/response_handlers/view.rb +2 -0
- data/lib/otto/response_handlers.rb +2 -2
- data/lib/otto/route.rb +4 -4
- data/lib/otto/route_definition.rb +42 -15
- data/lib/otto/route_handlers/base.rb +2 -0
- data/lib/otto/route_handlers/class_method.rb +26 -26
- data/lib/otto/route_handlers/factory.rb +2 -2
- data/lib/otto/route_handlers/instance_method.rb +16 -6
- data/lib/otto/route_handlers/lambda.rb +8 -20
- data/lib/otto/route_handlers/logic_class.rb +33 -8
- data/lib/otto/route_handlers.rb +2 -2
- data/lib/otto/security/authentication/auth_failure.rb +2 -2
- data/lib/otto/security/authentication/auth_strategy.rb +11 -4
- data/lib/otto/security/authentication/route_auth_wrapper/response_builder.rb +123 -0
- data/lib/otto/security/authentication/route_auth_wrapper/role_authorization.rb +120 -0
- data/lib/otto/security/authentication/route_auth_wrapper/strategy_resolver.rb +69 -0
- data/lib/otto/security/authentication/route_auth_wrapper.rb +185 -195
- data/lib/otto/security/authentication/strategies/api_key_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategies/noauth_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategies/permission_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategies/role_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategies/session_strategy.rb +2 -0
- data/lib/otto/security/authentication/strategy_result.rb +6 -5
- data/lib/otto/security/authentication.rb +2 -2
- data/lib/otto/security/authorization_error.rb +73 -0
- data/lib/otto/security/config.rb +2 -2
- data/lib/otto/security/configurator.rb +17 -2
- data/lib/otto/security/csrf.rb +2 -2
- data/lib/otto/security/middleware/csrf_middleware.rb +11 -1
- data/lib/otto/security/middleware/ip_privacy_middleware.rb +31 -11
- data/lib/otto/security/middleware/rate_limit_middleware.rb +2 -0
- data/lib/otto/security/middleware/validation_middleware.rb +15 -0
- data/lib/otto/security/rate_limiter.rb +2 -2
- data/lib/otto/security/rate_limiting.rb +2 -2
- data/lib/otto/security/validator.rb +2 -2
- data/lib/otto/security.rb +3 -0
- data/lib/otto/static.rb +2 -2
- data/lib/otto/utils.rb +27 -2
- data/lib/otto/version.rb +3 -3
- data/lib/otto.rb +174 -14
- data/otto.gemspec +7 -3
- metadata +25 -15
- data/benchmark_middleware_wrap.rb +0 -163
- data/changelog.d/20251014_144317_delano_54_thats_a_wrapper.rst +0 -36
- data/changelog.d/20251014_161526_delano_54_thats_a_wrapper.rst +0 -5
|
@@ -1,32 +1,225 @@
|
|
|
1
1
|
# Otto - Authentication Strategies Example
|
|
2
2
|
|
|
3
|
-
This example demonstrates
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
196
|
+
To add your own authentication:
|
|
13
197
|
|
|
14
|
-
1.
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
208
|
+
2. **Register it in config.ru**:
|
|
209
|
+
```ruby
|
|
210
|
+
app.add_auth_strategy('my_strategy', MyStrategy.new)
|
|
211
|
+
```
|
|
20
212
|
|
|
21
|
-
3.
|
|
213
|
+
3. **Use it in routes**:
|
|
214
|
+
```
|
|
215
|
+
GET /protected Controller#method auth=my_strategy
|
|
216
|
+
```
|
|
22
217
|
|
|
23
|
-
##
|
|
218
|
+
## Next Steps
|
|
24
219
|
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
225
|
+
- [CLAUDE.md](../../CLAUDE.md#authentication-architecture) - Detailed auth documentation
|
|
@@ -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!"
|
data/examples/basic/README.md
CHANGED
|
@@ -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
|
-
|
|
15
|
+
### Using rackup (recommended)
|
|
16
|
+
|
|
8
17
|
```sh
|
|
9
|
-
|
|
18
|
+
cd examples/basic
|
|
19
|
+
rackup config.ru -p 10770
|
|
10
20
|
```
|
|
11
21
|
|
|
12
|
-
|
|
22
|
+
### Using thin
|
|
23
|
+
|
|
13
24
|
```sh
|
|
14
|
-
|
|
25
|
+
cd examples/basic
|
|
26
|
+
thin -e dev -R config.ru -p 10770 start
|
|
15
27
|
```
|
|
16
28
|
|
|
17
|
-
|
|
29
|
+
### Using puma
|
|
30
|
+
|
|
18
31
|
```sh
|
|
19
|
-
|
|
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
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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}"
|