aris 1.4.2 → 1.5.0
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 +251 -0
- data/README.md +18 -0
- data/docs/ADAPTERS.md +478 -0
- data/docs/ARCHITECTURE.md +222 -0
- data/docs/CONTENT.md +967 -0
- data/docs/PERFORMANCE.md +492 -0
- data/docs/PLUGIN_DEVELOPMENT.md +688 -0
- data/docs/USAGE.md +4998 -0
- data/docs/plugins/API_KEY_AUTH.md +232 -0
- data/docs/plugins/BASIC_AUTH.md +582 -0
- data/docs/plugins/BEARER_AUTH.md +394 -0
- data/docs/plugins/CACHE.md +369 -0
- data/docs/plugins/COMPRESSION.md +216 -0
- data/docs/plugins/COOKIES.md +30 -0
- data/docs/plugins/CORS.md +283 -0
- data/docs/plugins/CSRF.md +751 -0
- data/docs/plugins/ETAG.md +308 -0
- data/docs/plugins/FORM_PARSER.md +193 -0
- data/docs/plugins/HEALTH_CHECK.md +469 -0
- data/docs/plugins/JSON.md +291 -0
- data/docs/plugins/MULTIPART.md +427 -0
- data/docs/plugins/RATE_LIMITER.md +368 -0
- data/docs/plugins/REQUEST_ID.md +369 -0
- data/docs/plugins/REQUEST_LOGGER.md +151 -0
- data/docs/plugins/SECURITY.md +193 -0
- data/docs/plugins/SESSION.md +98 -0
- data/lib/aris/adapters/rack/adapter.rb +17 -2
- data/lib/aris/adapters/rack/request.rb +29 -11
- data/lib/aris/plugins/basic_auth.rb +3 -1
- data/lib/aris/plugins/cookies.rb +4 -32
- data/lib/aris/plugins/cors.rb +8 -1
- data/lib/aris/plugins/csrf.rb +63 -22
- data/lib/aris/plugins/flash.rb +3 -1
- data/lib/aris/plugins/form_parser.rb +52 -31
- data/lib/aris/plugins/multipart.rb +22 -2
- data/lib/aris/plugins/request_logger.rb +8 -1
- data/lib/aris/plugins/security_headers.rb +8 -1
- data/lib/aris/plugins/session.rb +150 -99
- data/lib/aris/response_helpers.rb +41 -0
- data/lib/aris/version.rb +2 -2
- metadata +31 -3
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
# Health Check Plugin
|
|
2
|
+
|
|
3
|
+
Provides a `/health` endpoint for monitoring, load balancers, and orchestration tools (Kubernetes, ECS, etc.). Returns service health status with optional dependency checks.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
require 'aris/plugins/health_check'
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Basic Usage
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
health = Aris::Plugins::HealthCheck.build
|
|
15
|
+
|
|
16
|
+
Aris.routes({
|
|
17
|
+
"api.example.com": {
|
|
18
|
+
use: [health], # Available at GET /health
|
|
19
|
+
"/users": { get: { to: UsersHandler } }
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
# GET /health
|
|
24
|
+
# => { "status": "ok", "name": "app", "checks": {}, "timestamp": "2025-10-10T14:23:45Z" }
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
| Option | Type | Default | Description |
|
|
30
|
+
|--------|------|---------|-------------|
|
|
31
|
+
| `path` | String | `'/health'` | Health check endpoint path |
|
|
32
|
+
| `checks` | Hash | `{}` | Health check functions (name → proc) |
|
|
33
|
+
| `name` | String | `'app'` | Service name |
|
|
34
|
+
| `version` | String | `nil` | Service version (optional) |
|
|
35
|
+
|
|
36
|
+
## Examples
|
|
37
|
+
|
|
38
|
+
### Simple Health Check
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
health = Aris::Plugins::HealthCheck.build
|
|
42
|
+
|
|
43
|
+
# GET /health
|
|
44
|
+
# Response: 200 OK
|
|
45
|
+
# {
|
|
46
|
+
# "status": "ok",
|
|
47
|
+
# "name": "app",
|
|
48
|
+
# "checks": {},
|
|
49
|
+
# "timestamp": "2025-10-10T14:23:45Z"
|
|
50
|
+
# }
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### With Database Check
|
|
54
|
+
|
|
55
|
+
```ruby
|
|
56
|
+
health = Aris::Plugins::HealthCheck.build(
|
|
57
|
+
checks: {
|
|
58
|
+
database: -> {
|
|
59
|
+
ActiveRecord::Base.connection.active?
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# GET /health
|
|
65
|
+
# Response: 200 OK (if DB healthy)
|
|
66
|
+
# {
|
|
67
|
+
# "status": "ok",
|
|
68
|
+
# "name": "app",
|
|
69
|
+
# "checks": {
|
|
70
|
+
# "database": "ok"
|
|
71
|
+
# },
|
|
72
|
+
# "timestamp": "2025-10-10T14:23:45Z"
|
|
73
|
+
# }
|
|
74
|
+
|
|
75
|
+
# Response: 503 Service Unavailable (if DB down)
|
|
76
|
+
# {
|
|
77
|
+
# "status": "degraded",
|
|
78
|
+
# "name": "app",
|
|
79
|
+
# "checks": {
|
|
80
|
+
# "database": "fail"
|
|
81
|
+
# },
|
|
82
|
+
# "timestamp": "2025-10-10T14:23:45Z"
|
|
83
|
+
# }
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Multiple Dependency Checks
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
health = Aris::Plugins::HealthCheck.build(
|
|
90
|
+
name: 'user-api',
|
|
91
|
+
version: '1.2.3',
|
|
92
|
+
checks: {
|
|
93
|
+
database: -> {
|
|
94
|
+
ActiveRecord::Base.connection.active?
|
|
95
|
+
},
|
|
96
|
+
redis: -> {
|
|
97
|
+
Redis.new.ping == 'PONG'
|
|
98
|
+
},
|
|
99
|
+
s3: -> {
|
|
100
|
+
AWS::S3.new.list_buckets.any?
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
# All healthy: 200 OK
|
|
106
|
+
# Any failing: 503 Service Unavailable
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Custom Path
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
health = Aris::Plugins::HealthCheck.build(
|
|
113
|
+
path: '/status'
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Available at GET /status instead of /health
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Kubernetes Liveness Probe
|
|
120
|
+
|
|
121
|
+
```ruby
|
|
122
|
+
health = Aris::Plugins::HealthCheck.build(
|
|
123
|
+
path: '/healthz',
|
|
124
|
+
name: 'my-service'
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# In kubernetes.yaml:
|
|
128
|
+
# livenessProbe:
|
|
129
|
+
# httpGet:
|
|
130
|
+
# path: /healthz
|
|
131
|
+
# port: 3000
|
|
132
|
+
# initialDelaySeconds: 10
|
|
133
|
+
# periodSeconds: 5
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### AWS ELB Health Check
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
health = Aris::Plugins::HealthCheck.build(
|
|
140
|
+
checks: {
|
|
141
|
+
database: -> { DB.ping }
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
# In ELB config:
|
|
146
|
+
# Health check path: /health
|
|
147
|
+
# Success codes: 200
|
|
148
|
+
# Unhealthy threshold: 2
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Check Function Patterns
|
|
152
|
+
|
|
153
|
+
### Database (ActiveRecord)
|
|
154
|
+
|
|
155
|
+
```ruby
|
|
156
|
+
database: -> {
|
|
157
|
+
ActiveRecord::Base.connection.active?
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Database (Sequel)
|
|
162
|
+
|
|
163
|
+
```ruby
|
|
164
|
+
database: -> {
|
|
165
|
+
DB.test_connection
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Redis
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
redis: -> {
|
|
173
|
+
Redis.new.ping == 'PONG'
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### External API
|
|
178
|
+
|
|
179
|
+
```ruby
|
|
180
|
+
payment_api: -> {
|
|
181
|
+
response = HTTParty.get('https://api.stripe.com/v1/status')
|
|
182
|
+
response.code == 200
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
### File System
|
|
187
|
+
|
|
188
|
+
```ruby
|
|
189
|
+
storage: -> {
|
|
190
|
+
File.writable?('/var/uploads')
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Memory Usage
|
|
195
|
+
|
|
196
|
+
```ruby
|
|
197
|
+
memory: -> {
|
|
198
|
+
# Check if memory usage is below threshold
|
|
199
|
+
`ps -o rss= -p #{Process.pid}`.to_i < 500_000 # 500MB
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Custom Logic
|
|
204
|
+
|
|
205
|
+
```ruby
|
|
206
|
+
workers: -> {
|
|
207
|
+
# Check if background workers are running
|
|
208
|
+
Sidekiq::ProcessSet.new.size > 0
|
|
209
|
+
}
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
## Response Format
|
|
213
|
+
|
|
214
|
+
### Healthy Response (200 OK)
|
|
215
|
+
|
|
216
|
+
```json
|
|
217
|
+
{
|
|
218
|
+
"status": "ok",
|
|
219
|
+
"name": "user-api",
|
|
220
|
+
"version": "1.2.3",
|
|
221
|
+
"checks": {
|
|
222
|
+
"database": "ok",
|
|
223
|
+
"redis": "ok"
|
|
224
|
+
},
|
|
225
|
+
"timestamp": "2025-10-10T14:23:45Z"
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Degraded Response (503 Service Unavailable)
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"status": "degraded",
|
|
234
|
+
"name": "user-api",
|
|
235
|
+
"version": "1.2.3",
|
|
236
|
+
"checks": {
|
|
237
|
+
"database": "ok",
|
|
238
|
+
"redis": "fail"
|
|
239
|
+
},
|
|
240
|
+
"timestamp": "2025-10-10T14:23:45Z"
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### Check Exception (503 Service Unavailable)
|
|
245
|
+
|
|
246
|
+
```json
|
|
247
|
+
{
|
|
248
|
+
"status": "degraded",
|
|
249
|
+
"name": "user-api",
|
|
250
|
+
"checks": {
|
|
251
|
+
"database": "error: Connection refused"
|
|
252
|
+
},
|
|
253
|
+
"timestamp": "2025-10-10T14:23:45Z"
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Production Tips
|
|
258
|
+
|
|
259
|
+
### 1. Keep Checks Fast
|
|
260
|
+
|
|
261
|
+
Health checks run on every probe (every 5-10 seconds):
|
|
262
|
+
|
|
263
|
+
```ruby
|
|
264
|
+
# ❌ BAD - Slow query
|
|
265
|
+
database: -> { User.count > 0 }
|
|
266
|
+
|
|
267
|
+
# ✅ GOOD - Fast connection test
|
|
268
|
+
database: -> { ActiveRecord::Base.connection.active? }
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### 2. Separate Liveness vs Readiness
|
|
272
|
+
|
|
273
|
+
**Liveness:** Is the app alive?
|
|
274
|
+
```ruby
|
|
275
|
+
liveness = HealthCheck.build(
|
|
276
|
+
path: '/healthz',
|
|
277
|
+
checks: {} # No dependency checks
|
|
278
|
+
)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
**Readiness:** Is the app ready to serve traffic?
|
|
282
|
+
```ruby
|
|
283
|
+
readiness = HealthCheck.build(
|
|
284
|
+
path: '/ready',
|
|
285
|
+
checks: {
|
|
286
|
+
database: -> { DB.ping },
|
|
287
|
+
redis: -> { Redis.new.ping == 'PONG' }
|
|
288
|
+
}
|
|
289
|
+
)
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### 3. Timeout Checks
|
|
293
|
+
|
|
294
|
+
```ruby
|
|
295
|
+
require 'timeout'
|
|
296
|
+
|
|
297
|
+
health = HealthCheck.build(
|
|
298
|
+
checks: {
|
|
299
|
+
database: -> {
|
|
300
|
+
Timeout.timeout(2) do # 2 second timeout
|
|
301
|
+
ActiveRecord::Base.connection.active?
|
|
302
|
+
end
|
|
303
|
+
rescue Timeout::Error
|
|
304
|
+
false
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
)
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### 4. Log Failed Checks
|
|
311
|
+
|
|
312
|
+
```ruby
|
|
313
|
+
health = HealthCheck.build(
|
|
314
|
+
checks: {
|
|
315
|
+
database: -> {
|
|
316
|
+
result = DB.ping
|
|
317
|
+
logger.error("Database health check failed") unless result
|
|
318
|
+
result
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
)
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### 5. Monitoring Integration
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
# Send metrics to DataDog/StatsD
|
|
328
|
+
health = HealthCheck.build(
|
|
329
|
+
checks: {
|
|
330
|
+
database: -> {
|
|
331
|
+
start = Time.now
|
|
332
|
+
result = DB.ping
|
|
333
|
+
duration = Time.now - start
|
|
334
|
+
|
|
335
|
+
StatsD.gauge('health.database.duration', duration)
|
|
336
|
+
StatsD.gauge('health.database.status', result ? 1 : 0)
|
|
337
|
+
|
|
338
|
+
result
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### 6. Security - Internal Only
|
|
345
|
+
|
|
346
|
+
Don't expose health to public:
|
|
347
|
+
|
|
348
|
+
```ruby
|
|
349
|
+
# Option A: Separate domain
|
|
350
|
+
internal_health = HealthCheck.build
|
|
351
|
+
|
|
352
|
+
Aris.routes({
|
|
353
|
+
"internal.api.com": { # Internal network only
|
|
354
|
+
use: [internal_health],
|
|
355
|
+
"/admin": { get: { to: AdminHandler } }
|
|
356
|
+
},
|
|
357
|
+
"api.com": { # Public
|
|
358
|
+
"/users": { get: { to: UsersHandler } }
|
|
359
|
+
}
|
|
360
|
+
})
|
|
361
|
+
|
|
362
|
+
# Option B: IP whitelist
|
|
363
|
+
# (Use firewall/load balancer rules)
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
## Common Patterns
|
|
367
|
+
|
|
368
|
+
### Kubernetes
|
|
369
|
+
|
|
370
|
+
```yaml
|
|
371
|
+
# deployment.yaml
|
|
372
|
+
livenessProbe:
|
|
373
|
+
httpGet:
|
|
374
|
+
path: /healthz
|
|
375
|
+
port: 3000
|
|
376
|
+
initialDelaySeconds: 10
|
|
377
|
+
periodSeconds: 10
|
|
378
|
+
|
|
379
|
+
readinessProbe:
|
|
380
|
+
httpGet:
|
|
381
|
+
path: /ready
|
|
382
|
+
port: 3000
|
|
383
|
+
initialDelaySeconds: 5
|
|
384
|
+
periodSeconds: 5
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
```ruby
|
|
388
|
+
# health_checks.rb
|
|
389
|
+
liveness = HealthCheck.build(path: '/healthz')
|
|
390
|
+
readiness = HealthCheck.build(
|
|
391
|
+
path: '/ready',
|
|
392
|
+
checks: {
|
|
393
|
+
database: -> { DB.ping },
|
|
394
|
+
redis: -> { Redis.new.ping == 'PONG' }
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
Aris.routes({
|
|
399
|
+
"api.example.com": {
|
|
400
|
+
use: [liveness, readiness],
|
|
401
|
+
"/users": { get: { to: UsersHandler } }
|
|
402
|
+
}
|
|
403
|
+
})
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Docker Compose
|
|
407
|
+
|
|
408
|
+
```yaml
|
|
409
|
+
# docker-compose.yml
|
|
410
|
+
services:
|
|
411
|
+
api:
|
|
412
|
+
healthcheck:
|
|
413
|
+
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
|
414
|
+
interval: 10s
|
|
415
|
+
timeout: 3s
|
|
416
|
+
retries: 3
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
### AWS ECS
|
|
420
|
+
|
|
421
|
+
```json
|
|
422
|
+
{
|
|
423
|
+
"healthCheck": {
|
|
424
|
+
"command": ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"],
|
|
425
|
+
"interval": 30,
|
|
426
|
+
"timeout": 5,
|
|
427
|
+
"retries": 3
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Uptime Monitoring (Pingdom, UptimeRobot)
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
# Simple endpoint, no dependency checks
|
|
436
|
+
health = HealthCheck.build
|
|
437
|
+
|
|
438
|
+
# Configure monitor:
|
|
439
|
+
# URL: https://api.example.com/health
|
|
440
|
+
# Expected: 200 OK
|
|
441
|
+
# Check frequency: 1 minute
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## Notes
|
|
445
|
+
|
|
446
|
+
- Halts plugin pipeline (returns immediately)
|
|
447
|
+
- Only responds to GET requests
|
|
448
|
+
- Check functions should be fast (<100ms)
|
|
449
|
+
- Failed checks return 503 (Service Unavailable)
|
|
450
|
+
- Exceptions in checks are caught and reported
|
|
451
|
+
- Thread-safe (check functions run synchronously)
|
|
452
|
+
|
|
453
|
+
## Troubleshooting
|
|
454
|
+
|
|
455
|
+
**Health check not responding?**
|
|
456
|
+
- Verify plugin is in `use:` array
|
|
457
|
+
- Check path matches (default: `/health`)
|
|
458
|
+
- Ensure GET request (POST won't work)
|
|
459
|
+
|
|
460
|
+
**Always returns 503?**
|
|
461
|
+
- Check which dependency is failing
|
|
462
|
+
- Look at `checks` field in response
|
|
463
|
+
- Add logging to check functions
|
|
464
|
+
|
|
465
|
+
**Slow health checks?**
|
|
466
|
+
- Profile check functions
|
|
467
|
+
- Add timeouts
|
|
468
|
+
- Remove expensive checks
|
|
469
|
+
- Consider separate liveness/readiness
|