rapitapir 0.1.1 → 2.0.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.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -7
  3. data/.rubocop_todo.yml +83 -0
  4. data/README.md +1319 -235
  5. data/RUBY_WEEKLY_LAUNCH_POST.md +219 -0
  6. data/docs/RAILS_INTEGRATION_IMPLEMENTATION.md +209 -0
  7. data/docs/SINATRA_EXTENSION.md +399 -348
  8. data/docs/STRICT_VALIDATION.md +229 -0
  9. data/docs/VALIDATION_IMPROVEMENTS.md +218 -0
  10. data/docs/ai-integration-plan.md +112 -0
  11. data/docs/auto-derivation.md +505 -92
  12. data/docs/endpoint-definition.md +536 -129
  13. data/docs/n8n-integration.md +212 -0
  14. data/docs/observability.md +810 -500
  15. data/docs/using-mcp.md +93 -0
  16. data/examples/ai/knowledge_base_rag.rb +83 -0
  17. data/examples/ai/user_management_mcp.rb +92 -0
  18. data/examples/ai/user_validation_llm.rb +187 -0
  19. data/examples/rails/RAILS_8_GUIDE.md +165 -0
  20. data/examples/rails/RAILS_LOADING_FIX.rb +35 -0
  21. data/examples/rails/README.md +497 -0
  22. data/examples/rails/comprehensive_test.rb +91 -0
  23. data/examples/rails/config/routes.rb +48 -0
  24. data/examples/rails/debug_controller.rb +63 -0
  25. data/examples/rails/detailed_test.rb +46 -0
  26. data/examples/rails/enhanced_users_controller.rb +278 -0
  27. data/examples/rails/final_server_test.rb +50 -0
  28. data/examples/rails/hello_world_app.rb +116 -0
  29. data/examples/rails/hello_world_controller.rb +186 -0
  30. data/examples/rails/hello_world_routes.rb +28 -0
  31. data/examples/rails/rails8_minimal_demo.rb +132 -0
  32. data/examples/rails/rails8_simple_demo.rb +140 -0
  33. data/examples/rails/rails8_working_demo.rb +255 -0
  34. data/examples/rails/real_world_blog_api.rb +510 -0
  35. data/examples/rails/server_test.rb +46 -0
  36. data/examples/rails/test_direct_processing.rb +41 -0
  37. data/examples/rails/test_hello_world.rb +80 -0
  38. data/examples/rails/test_rails_integration.rb +54 -0
  39. data/examples/rails/traditional_app/Gemfile +37 -0
  40. data/examples/rails/traditional_app/README.md +265 -0
  41. data/examples/rails/traditional_app/app/controllers/api/v1/posts_controller.rb +254 -0
  42. data/examples/rails/traditional_app/app/controllers/api/v1/users_controller.rb +220 -0
  43. data/examples/rails/traditional_app/app/controllers/application_controller.rb +86 -0
  44. data/examples/rails/traditional_app/app/controllers/application_controller_simplified.rb +87 -0
  45. data/examples/rails/traditional_app/app/controllers/documentation_controller.rb +149 -0
  46. data/examples/rails/traditional_app/app/controllers/health_controller.rb +42 -0
  47. data/examples/rails/traditional_app/config/routes.rb +25 -0
  48. data/examples/rails/traditional_app/config/routes_best_practice.rb +25 -0
  49. data/examples/rails/traditional_app/config/routes_simplified.rb +36 -0
  50. data/examples/rails/traditional_app_runnable.rb +406 -0
  51. data/examples/rails/users_controller.rb +4 -1
  52. data/examples/serverless/Gemfile +43 -0
  53. data/examples/serverless/QUICKSTART.md +331 -0
  54. data/examples/serverless/README.md +520 -0
  55. data/examples/serverless/aws_lambda_example.rb +307 -0
  56. data/examples/serverless/aws_sam_template.yaml +215 -0
  57. data/examples/serverless/azure_functions_example.rb +407 -0
  58. data/examples/serverless/deploy.rb +204 -0
  59. data/examples/serverless/gcp_cloud_functions_example.rb +367 -0
  60. data/examples/serverless/gcp_function.yaml +23 -0
  61. data/examples/serverless/host.json +24 -0
  62. data/examples/serverless/package.json +32 -0
  63. data/examples/serverless/spec/aws_lambda_spec.rb +196 -0
  64. data/examples/serverless/spec/spec_helper.rb +89 -0
  65. data/examples/serverless/vercel.json +31 -0
  66. data/examples/serverless/vercel_example.rb +404 -0
  67. data/examples/strict_validation_examples.rb +104 -0
  68. data/examples/validation_error_examples.rb +173 -0
  69. data/lib/rapitapir/ai/llm_instruction.rb +456 -0
  70. data/lib/rapitapir/ai/mcp.rb +134 -0
  71. data/lib/rapitapir/ai/rag.rb +287 -0
  72. data/lib/rapitapir/ai/rag_middleware.rb +147 -0
  73. data/lib/rapitapir/auth/oauth2.rb +43 -57
  74. data/lib/rapitapir/cli/command.rb +362 -2
  75. data/lib/rapitapir/cli/mcp_export.rb +18 -0
  76. data/lib/rapitapir/cli/validator.rb +2 -6
  77. data/lib/rapitapir/core/endpoint.rb +59 -6
  78. data/lib/rapitapir/core/enhanced_endpoint.rb +2 -6
  79. data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +53 -0
  80. data/lib/rapitapir/endpoint_registry.rb +47 -0
  81. data/lib/rapitapir/observability/health_check.rb +4 -4
  82. data/lib/rapitapir/observability/logging.rb +10 -10
  83. data/lib/rapitapir/schema.rb +2 -2
  84. data/lib/rapitapir/server/rack_adapter.rb +1 -3
  85. data/lib/rapitapir/server/rails/configuration.rb +77 -0
  86. data/lib/rapitapir/server/rails/controller_base.rb +185 -0
  87. data/lib/rapitapir/server/rails/documentation_helpers.rb +76 -0
  88. data/lib/rapitapir/server/rails/resource_builder.rb +181 -0
  89. data/lib/rapitapir/server/rails/routes.rb +114 -0
  90. data/lib/rapitapir/server/rails_adapter.rb +10 -3
  91. data/lib/rapitapir/server/rails_adapter_class.rb +1 -3
  92. data/lib/rapitapir/server/rails_controller.rb +1 -3
  93. data/lib/rapitapir/server/rails_integration.rb +67 -0
  94. data/lib/rapitapir/server/rails_response_handler.rb +16 -3
  95. data/lib/rapitapir/server/sinatra_adapter.rb +29 -5
  96. data/lib/rapitapir/server/sinatra_integration.rb +4 -4
  97. data/lib/rapitapir/sinatra/extension.rb +2 -2
  98. data/lib/rapitapir/sinatra/oauth2_helpers.rb +34 -40
  99. data/lib/rapitapir/types/array.rb +4 -0
  100. data/lib/rapitapir/types/auto_derivation.rb +4 -18
  101. data/lib/rapitapir/types/datetime.rb +1 -3
  102. data/lib/rapitapir/types/float.rb +2 -6
  103. data/lib/rapitapir/types/hash.rb +40 -2
  104. data/lib/rapitapir/types/integer.rb +4 -12
  105. data/lib/rapitapir/types/object.rb +6 -2
  106. data/lib/rapitapir/types.rb +6 -2
  107. data/lib/rapitapir/version.rb +1 -1
  108. data/lib/rapitapir.rb +5 -3
  109. data/rapitapir.gemspec +7 -5
  110. metadata +116 -16
@@ -1,647 +1,957 @@
1
1
  # RapiTapir Observability Guide
2
2
 
3
- This guide covers the comprehensive observability features introduced in RapiTapir Phase 2.1, including metrics collection, distributed tracing, structured logging, and health checks.
3
+ This comprehensive guide covers RapiTapir's observability features including metrics collection, distributed tracing, structured logging, and health checks for production-ready APIs.
4
4
 
5
5
  ## Table of Contents
6
6
 
7
+ - [Overview](#overview)
7
8
  - [Quick Start](#quick-start)
8
9
  - [Configuration](#configuration)
9
10
  - [Metrics](#metrics)
10
11
  - [Distributed Tracing](#distributed-tracing)
11
12
  - [Structured Logging](#structured-logging)
12
13
  - [Health Checks](#health-checks)
13
- - [Middleware Integration](#middleware-integration)
14
- - [Examples](#examples)
14
+ - [Integration Examples](#integration-examples)
15
+ - [Production Setup](#production-setup)
15
16
 
16
- ## Quick Start
17
-
18
- ```ruby
19
- require 'rapitapir'
20
-
21
- # Configure observability
22
- RapiTapir.configure do |config|
23
- # Enable Prometheus metrics
24
- config.metrics.enable_prometheus
25
-
26
- # Enable OpenTelemetry tracing
27
- config.tracing.enable_opentelemetry
28
-
29
- # Enable structured logging
30
- config.logging.enable_structured
31
-
32
- # Enable health checks
33
- config.health_check.enable
34
- end
17
+ ## Overview
35
18
 
36
- # Create an endpoint with observability
37
- endpoint = RapiTapir.endpoint
38
- .get
39
- .in("/users")
40
- .out_json({ users: [{ id: :uuid, name: :string }] })
41
- .with_metrics("user_list")
42
- .with_tracing
43
- .with_logging(level: :info)
44
- .handle do |request|
45
- # Your endpoint logic here
46
- { users: [] }
47
- end
48
- ```
19
+ RapiTapir provides comprehensive observability features out of the box:
49
20
 
50
- ## Configuration
21
+ - **Metrics**: Prometheus-compatible metrics with custom labels and dashboards
22
+ - **Tracing**: OpenTelemetry distributed tracing with automatic instrumentation
23
+ - **Logging**: Structured JSON logging with request correlation
24
+ - **Health Checks**: Built-in health monitoring with custom checks
25
+ - **Performance Monitoring**: Request duration, throughput, and error rate tracking
51
26
 
52
- ### Metrics Configuration
27
+ ## Quick Start
53
28
 
54
29
  ```ruby
55
- RapiTapir.configure do |config|
56
- config.metrics.enable_prometheus(
57
- namespace: 'my_api', # Metrics namespace (default: 'rapitapir')
58
- labels: { # Custom labels for all metrics
59
- service: 'user_service',
60
- version: '1.0.0',
61
- environment: 'production'
62
- }
63
- )
64
- end
65
- ```
30
+ class ObservableAPI < SinatraRapiTapir
31
+ rapitapir do
32
+ info(title: 'Observable API', version: '1.0.0')
33
+
34
+ # Enable observability features
35
+ enable_metrics(
36
+ provider: :prometheus,
37
+ namespace: 'my_api',
38
+ labels: { service: 'user_service', environment: 'production' }
39
+ )
40
+
41
+ enable_tracing(
42
+ provider: :opentelemetry,
43
+ service_name: 'user-api',
44
+ service_version: '1.0.0'
45
+ )
46
+
47
+ enable_structured_logging(
48
+ level: :info,
49
+ include_request_body: false,
50
+ include_response_body: false
51
+ )
52
+
53
+ enable_health_checks(endpoint: '/health')
54
+
55
+ production_defaults!
56
+ end
66
57
 
67
- ### Tracing Configuration
58
+ # Basic endpoint with automatic observability
59
+ endpoint(
60
+ GET('/users')
61
+ .summary('List users with observability')
62
+ .query(:limit, T.optional(T.integer(minimum: 1, maximum: 100)), description: 'Page size')
63
+ .tags('Users', 'Observability')
64
+ .ok(T.hash({
65
+ "users" => T.array(T.hash({
66
+ "id" => T.uuid,
67
+ "name" => T.string,
68
+ "email" => T.email,
69
+ "created_at" => T.datetime
70
+ })),
71
+ "total" => T.integer,
72
+ "page_info" => T.hash({
73
+ "limit" => T.integer,
74
+ "has_next" => T.boolean
75
+ })
76
+ }))
77
+ .build
78
+ ) do |inputs|
79
+ # Automatic metrics and tracing are enabled
80
+ users = User.limit(inputs[:limit] || 50)
81
+
82
+ {
83
+ users: users.map(&:to_h),
84
+ total: User.count,
85
+ page_info: {
86
+ limit: inputs[:limit] || 50,
87
+ has_next: users.length == (inputs[:limit] || 50)
88
+ }
89
+ }
90
+ end
68
91
 
69
- ```ruby
70
- RapiTapir.configure do |config|
71
- config.tracing.enable_opentelemetry(
72
- service_name: 'my-api-service', # Service name for tracing
73
- service_version: '1.0.0' # Service version
74
- )
92
+ # Enhanced endpoint with custom observability
93
+ endpoint(
94
+ POST('/orders')
95
+ .summary('Create order with enhanced observability')
96
+ .body(T.hash({
97
+ "user_id" => T.uuid,
98
+ "items" => T.array(T.hash({
99
+ "product_id" => T.uuid,
100
+ "quantity" => T.integer(minimum: 1),
101
+ "price" => T.float(minimum: 0)
102
+ })),
103
+ "payment_method" => T.string(enum: %w[card paypal bank_transfer])
104
+ }))
105
+ .tags('Orders', 'Observability')
106
+ .created(T.hash({
107
+ "id" => T.uuid,
108
+ "status" => T.string,
109
+ "total" => T.float,
110
+ "created_at" => T.datetime
111
+ }))
112
+ .with_metrics('order_creation', labels: { operation: 'create' })
113
+ .with_tracing('create_order')
114
+ .with_structured_logging(
115
+ include_fields: %w[user_id total payment_method],
116
+ exclude_fields: %w[payment_details]
117
+ )
118
+ .build
119
+ ) do |inputs|
120
+ order_data = inputs[:body]
121
+
122
+ # Add custom metrics
123
+ increment_counter('orders_attempted_total', labels: {
124
+ payment_method: order_data['payment_method']
125
+ })
126
+
127
+ # Add tracing attributes
128
+ set_trace_attributes({
129
+ 'order.user_id' => order_data['user_id'],
130
+ 'order.item_count' => order_data['items'].length,
131
+ 'order.payment_method' => order_data['payment_method']
132
+ })
133
+
134
+ # Create nested spans for different operations
135
+ user = trace_span('fetch_user') do |span|
136
+ span.set_attribute('user.id', order_data['user_id'])
137
+ User.find(order_data['user_id'])
138
+ end
139
+
140
+ total = trace_span('calculate_total') do |span|
141
+ calculated_total = order_data['items'].sum { |item|
142
+ item['quantity'] * item['price']
143
+ }
144
+ span.set_attribute('order.calculated_total', calculated_total)
145
+ calculated_total
146
+ end
147
+
148
+ order = trace_span('process_payment') do |span|
149
+ span.set_attribute('payment.method', order_data['payment_method'])
150
+ span.set_attribute('payment.amount', total)
151
+
152
+ payment_result = PaymentService.process(
153
+ amount: total,
154
+ method: order_data['payment_method'],
155
+ user: user
156
+ )
157
+
158
+ span.set_attribute('payment.transaction_id', payment_result[:transaction_id])
159
+ span.add_event('payment_processed', {
160
+ 'payment.status' => payment_result[:status],
161
+ 'payment.provider' => payment_result[:provider]
162
+ })
163
+
164
+ Order.create!(
165
+ user: user,
166
+ items: order_data['items'],
167
+ total: total,
168
+ payment_transaction_id: payment_result[:transaction_id]
169
+ )
170
+ end
171
+
172
+ # Record custom metrics
173
+ histogram('order_total_amount', total, labels: {
174
+ payment_method: order_data['payment_method']
175
+ })
176
+
177
+ # Log structured data
178
+ log_info('Order created successfully', {
179
+ order_id: order.id,
180
+ user_id: user.id,
181
+ total: total,
182
+ payment_method: order_data['payment_method']
183
+ })
184
+
185
+ status 201
186
+ order.to_h
187
+ end
75
188
  end
76
189
  ```
77
190
 
78
- ### Logging Configuration
191
+ ## Configuration
192
+
193
+ ### Global Configuration
79
194
 
80
195
  ```ruby
81
- RapiTapir.configure do |config|
82
- config.logging.enable_structured(
83
- level: :info, # Log level (:debug, :info, :warn, :error, :fatal)
84
- fields: [ # Fields to include in structured logs
85
- :timestamp, :level, :message, :request_id,
86
- :method, :path, :status, :duration,
87
- :user_id, :tenant_id # Custom fields
88
- ]
89
- )
196
+ class MyAPI < SinatraRapiTapir
197
+ rapitapir do
198
+ # Comprehensive observability setup
199
+ enable_observability do |obs|
200
+ # Metrics configuration
201
+ obs.metrics do |metrics|
202
+ metrics.provider = :prometheus
203
+ metrics.namespace = 'myapi'
204
+ metrics.endpoint = '/metrics'
205
+ metrics.labels = {
206
+ service: 'user-service',
207
+ version: '2.0.0',
208
+ environment: ENV['RACK_ENV'] || 'development'
209
+ }
210
+ metrics.include_default_metrics = true
211
+ metrics.include_request_metrics = true
212
+ metrics.include_response_metrics = true
213
+ end
214
+
215
+ # Tracing configuration
216
+ obs.tracing do |tracing|
217
+ tracing.provider = :opentelemetry
218
+ tracing.service_name = 'user-api'
219
+ tracing.service_version = '2.0.0'
220
+ tracing.environment = ENV['RACK_ENV']
221
+ tracing.sample_rate = ENV['RACK_ENV'] == 'production' ? 0.1 : 1.0
222
+ tracing.include_request_attributes = true
223
+ tracing.include_response_attributes = true
224
+ tracing.include_database_spans = true
225
+ end
226
+
227
+ # Logging configuration
228
+ obs.logging do |logging|
229
+ logging.level = ENV['LOG_LEVEL'] || 'info'
230
+ logging.format = :json
231
+ logging.include_request_id = true
232
+ logging.include_user_id = true
233
+ logging.include_trace_id = true
234
+ logging.exclude_paths = ['/health', '/metrics']
235
+ logging.sanitize_headers = %w[authorization x-api-key]
236
+ logging.max_body_size = 1024
237
+ end
238
+
239
+ # Health checks configuration
240
+ obs.health_checks do |health|
241
+ health.endpoint = '/health'
242
+ health.detailed_endpoint = '/health/detailed'
243
+ health.include_system_info = true
244
+ health.include_dependency_checks = true
245
+ end
246
+ end
247
+ end
90
248
  end
91
249
  ```
92
250
 
93
- ### Health Check Configuration
251
+ ### Environment-Specific Configuration
94
252
 
95
253
  ```ruby
96
- RapiTapir.configure do |config|
97
- config.health_check.enable(endpoint: '/health')
98
-
99
- # Add custom health checks
100
- config.health_check.add_check(:database) do
101
- # Database health check logic
102
- { status: :healthy, message: 'Database connection OK' }
103
- end
104
-
105
- config.health_check.add_check(:redis) do
106
- # Redis health check logic
107
- { status: :healthy, message: 'Redis connection OK' }
254
+ class ProductionAPI < SinatraRapiTapir
255
+ rapitapir do
256
+ case ENV['RACK_ENV']
257
+ when 'production'
258
+ enable_observability do |obs|
259
+ obs.metrics.sample_rate = 1.0
260
+ obs.tracing.sample_rate = 0.1 # Sample 10% in production
261
+ obs.logging.level = 'warn'
262
+ obs.logging.include_request_body = false
263
+ obs.logging.include_response_body = false
264
+ end
265
+
266
+ when 'staging'
267
+ enable_observability do |obs|
268
+ obs.metrics.sample_rate = 1.0
269
+ obs.tracing.sample_rate = 0.5 # Sample 50% in staging
270
+ obs.logging.level = 'info'
271
+ obs.logging.include_request_body = true
272
+ obs.logging.include_response_body = false
273
+ end
274
+
275
+ when 'development'
276
+ enable_observability do |obs|
277
+ obs.metrics.sample_rate = 1.0
278
+ obs.tracing.sample_rate = 1.0 # Sample 100% in development
279
+ obs.logging.level = 'debug'
280
+ obs.logging.include_request_body = true
281
+ obs.logging.include_response_body = true
282
+ end
283
+ end
108
284
  end
109
285
  end
110
286
  ```
111
287
 
112
288
  ## Metrics
113
289
 
114
- RapiTapir automatically collects the following metrics:
290
+ ### Built-in Metrics
115
291
 
116
- ### Default HTTP Metrics
292
+ RapiTapir automatically collects these metrics:
117
293
 
118
- - `{namespace}_http_requests_total` - Total number of HTTP requests
119
- - `{namespace}_http_request_duration_seconds` - HTTP request duration histogram
120
- - `{namespace}_http_errors_total` - Total number of HTTP errors
121
- - `{namespace}_http_active_requests` - Number of active HTTP requests
294
+ - `http_requests_total` - Total HTTP requests by method, path, status
295
+ - `http_request_duration_seconds` - Request duration histogram
296
+ - `http_request_size_bytes` - Request size histogram
297
+ - `http_response_size_bytes` - Response size histogram
298
+ - `rapitapir_endpoints_total` - Total defined endpoints
299
+ - `rapitapir_validations_total` - Total validation operations
300
+ - `rapitapir_validation_errors_total` - Total validation errors
122
301
 
123
- ### Custom Metrics
124
-
125
- You can record custom metrics in your endpoint handlers:
302
+ ### Custom Metrics in Endpoints
126
303
 
127
304
  ```ruby
128
- endpoint = RapiTapir.endpoint
129
- .post
130
- .in("/orders")
131
- .with_metrics("order_creation")
132
- .handle do |request|
133
- # Custom counter
134
- RapiTapir::Observability::Metrics.registry
135
- .counter(:custom_events_total, labels: [:event_type])
136
- .increment(labels: { event_type: 'order_created' })
137
-
138
- # Custom histogram
139
- duration = measure_time do
140
- # Some operation
141
- end
142
-
143
- RapiTapir::Observability::Metrics.registry
144
- .histogram(:operation_duration_seconds, labels: [:operation])
145
- .observe(duration, labels: { operation: 'order_processing' })
146
-
147
- # Your logic here
305
+ endpoint(
306
+ GET('/analytics/dashboard')
307
+ .summary('Analytics dashboard with custom metrics')
308
+ .query(:timeframe, T.string(enum: %w[hour day week month]), description: 'Analytics timeframe')
309
+ .tags('Analytics')
310
+ .ok(T.hash({
311
+ "metrics" => T.hash({}),
312
+ "generated_at" => T.datetime
313
+ }))
314
+ .build
315
+ ) do |inputs|
316
+ timeframe = inputs[:timeframe] || 'day'
317
+
318
+ # Increment custom counters
319
+ increment_counter('dashboard_views_total', labels: {
320
+ timeframe: timeframe,
321
+ user_id: current_user&.id || 'anonymous'
322
+ })
323
+
324
+ # Record custom gauge
325
+ set_gauge('active_users_count', User.active.count)
326
+
327
+ # Record histogram
328
+ analytics_data = measure_histogram('analytics_query_duration',
329
+ labels: { timeframe: timeframe }
330
+ ) do
331
+ AnalyticsService.get_dashboard_data(timeframe: timeframe)
148
332
  end
333
+
334
+ # Record summary
335
+ record_summary('dashboard_data_points', analytics_data[:data_points].count)
336
+
337
+ {
338
+ metrics: analytics_data,
339
+ generated_at: Time.now
340
+ }
341
+ end
149
342
  ```
150
343
 
151
344
  ### Accessing Metrics
152
345
 
153
- Metrics are exposed at `/metrics` endpoint in Prometheus format:
346
+ Metrics are automatically exposed at `/metrics` endpoint:
154
347
 
155
348
  ```bash
156
- curl http://localhost:9292/metrics
349
+ # Get all metrics
350
+ curl http://localhost:4567/metrics
351
+
352
+ # Example output:
353
+ # http_requests_total{method="GET",path="/users",status="200"} 42
354
+ # http_request_duration_seconds_bucket{method="GET",path="/users",le="0.1"} 38
355
+ # http_request_duration_seconds_bucket{method="GET",path="/users",le="0.5"} 42
356
+ # dashboard_views_total{timeframe="day",user_id="123"} 15
157
357
  ```
158
358
 
159
359
  ## Distributed Tracing
160
360
 
161
- RapiTapir integrates with OpenTelemetry for distributed tracing:
162
-
163
361
  ### Automatic Tracing
164
362
 
165
- Every HTTP request is automatically traced with:
166
- - Span name: `HTTP {METHOD} {PATH}`
167
- - HTTP method, URL, status code, duration
168
- - Request and response size
169
- - Error information if applicable
363
+ Every request gets automatic tracing with:
170
364
 
171
- ### Custom Tracing
365
+ - Span name: `HTTP {METHOD} {path_template}`
366
+ - Automatic attributes: method, URL, status, duration, user agent
367
+ - Request/response size tracking
368
+ - Error tracking with stack traces
172
369
 
173
- Add custom spans and attributes in your endpoints:
370
+ ### Custom Tracing in Endpoints
174
371
 
175
372
  ```ruby
176
- endpoint = RapiTapir.endpoint
177
- .post
178
- .in("/orders")
179
- .with_tracing("POST /orders")
180
- .handle do |request|
181
- # Add custom attributes to current span
182
- RapiTapir::Observability::Tracing.set_attribute('user.id', request.user_id)
183
- RapiTapir::Observability::Tracing.set_attribute('order.total', request.body[:total])
373
+ endpoint(
374
+ POST('/orders/:id/process')
375
+ .path_param(:id, T.uuid, description: 'Order ID')
376
+ .body(T.hash({
377
+ "processing_options" => T.hash({
378
+ "priority" => T.string(enum: %w[low normal high urgent]),
379
+ "async" => T.boolean
380
+ })
381
+ }))
382
+ .tags('Orders')
383
+ .ok(T.hash({
384
+ "order_id" => T.uuid,
385
+ "status" => T.string,
386
+ "processing_time_ms" => T.float
387
+ }))
388
+ .build
389
+ ) do |inputs|
390
+ order_id = inputs[:id]
391
+ options = inputs[:body]['processing_options']
392
+ start_time = Time.now
393
+
394
+ # Set span attributes
395
+ set_trace_attributes({
396
+ 'order.id' => order_id,
397
+ 'order.priority' => options['priority'],
398
+ 'order.async' => options['async']
399
+ })
400
+
401
+ # Create nested spans
402
+ order = trace_span('fetch_order', attributes: { 'order.id' => order_id }) do |span|
403
+ order = Order.find(order_id)
404
+ span.set_attribute('order.status', order.status)
405
+ span.set_attribute('order.total', order.total)
406
+ order
407
+ end
408
+
409
+ validation_result = trace_span('validate_processing') do |span|
410
+ result = OrderValidator.can_process?(order, options)
411
+ span.set_attribute('validation.result', result[:valid])
412
+ span.set_attribute('validation.errors', result[:errors].join(', ')) if result[:errors]
184
413
 
185
- # Create nested spans
186
- RapiTapir::Observability::Tracing.start_span("validate_order") do |span|
187
- span.set_attribute('validation.type', 'business_rules')
188
- validate_order(request.body)
414
+ unless result[:valid]
415
+ span.record_exception(ValidationError.new(result[:errors].join(', ')))
416
+ span.set_status(:error, 'Validation failed')
189
417
  end
190
418
 
191
- RapiTapir::Observability::Tracing.start_span("process_payment") do |span|
192
- payment_result = process_payment(request.body[:payment])
193
- span.set_attribute('payment.provider', payment_result[:provider])
194
- span.set_attribute('payment.transaction_id', payment_result[:transaction_id])
419
+ result
420
+ end
421
+
422
+ halt 422, { error: 'Validation failed', details: validation_result[:errors] }.to_json unless validation_result[:valid]
423
+
424
+ processed_order = trace_span('process_order') do |span|
425
+ span.set_attribute('processing.priority', options['priority'])
426
+ span.set_attribute('processing.async', options['async'])
427
+
428
+ if options['async']
429
+ # Enqueue background job
430
+ job_id = OrderProcessingJob.perform_async(order_id, options)
431
+ span.set_attribute('job.id', job_id)
432
+ span.add_event('job_enqueued', { 'job.id' => job_id })
433
+
434
+ order.update!(status: 'processing', processing_job_id: job_id)
435
+ else
436
+ # Process synchronously
437
+ OrderProcessor.process!(order, options)
438
+ order.reload
195
439
  end
196
440
 
197
- # Add events to span
198
- RapiTapir::Observability::Tracing.add_event(
199
- 'order.created',
200
- attributes: { 'order.id' => order_id }
201
- )
441
+ span.set_attribute('order.new_status', order.status)
442
+ order
443
+ end
444
+
445
+ processing_time = ((Time.now - start_time) * 1000).round(2)
446
+
447
+ # Add final event
448
+ add_trace_event('order_processing_completed', {
449
+ 'order.id' => order_id,
450
+ 'processing.duration_ms' => processing_time,
451
+ 'processing.mode' => options['async'] ? 'async' : 'sync'
452
+ })
453
+
454
+ {
455
+ order_id: order_id,
456
+ status: processed_order.status,
457
+ processing_time_ms: processing_time
458
+ }
459
+ end
460
+ ```
461
+
462
+ ### Cross-Service Tracing
463
+
464
+ ```ruby
465
+ endpoint(
466
+ GET('/users/:id/recommendations')
467
+ .path_param(:id, T.uuid, description: 'User ID')
468
+ .query(:category, T.optional(T.string), description: 'Recommendation category')
469
+ .tags('Users', 'Recommendations')
470
+ .ok(T.hash({
471
+ "recommendations" => T.array(T.hash({
472
+ "id" => T.uuid,
473
+ "title" => T.string,
474
+ "score" => T.float
475
+ }))
476
+ }))
477
+ .build
478
+ ) do |inputs|
479
+ user_id = inputs[:id]
480
+
481
+ # External service call with tracing
482
+ recommendations = trace_span('fetch_recommendations') do |span|
483
+ span.set_attribute('user.id', user_id)
484
+ span.set_attribute('service.name', 'recommendation-service')
485
+
486
+ # Propagate trace context to external service
487
+ headers = {
488
+ 'Content-Type' => 'application/json',
489
+ 'X-Trace-Id' => current_trace_id,
490
+ 'X-Span-Id' => current_span_id
491
+ }
202
492
 
203
- # Record exceptions
204
493
  begin
205
- risky_operation()
494
+ response = HTTP.timeout(5)
495
+ .headers(headers)
496
+ .get("#{ENV['RECOMMENDATION_SERVICE_URL']}/users/#{user_id}/recommendations")
497
+
498
+ span.set_attribute('http.status_code', response.status)
499
+ span.set_attribute('http.response_size', response.body.bytesize)
500
+
501
+ if response.status.success?
502
+ recommendations = JSON.parse(response.body)
503
+ span.set_attribute('recommendations.count', recommendations.length)
504
+ recommendations
505
+ else
506
+ span.set_status(:error, "HTTP #{response.status}")
507
+ span.record_exception(StandardError.new("Recommendation service error: #{response.status}"))
508
+ []
509
+ end
510
+
206
511
  rescue => e
207
- RapiTapir::Observability::Tracing.record_exception(e)
208
- raise
512
+ span.record_exception(e)
513
+ span.set_status(:error, e.message)
514
+ []
209
515
  end
210
-
211
- # Your logic here
212
516
  end
517
+
518
+ { recommendations: recommendations }
519
+ end
213
520
  ```
214
521
 
215
522
  ## Structured Logging
216
523
 
217
- RapiTapir provides comprehensive structured logging:
218
-
219
524
  ### Automatic Request Logging
220
525
 
221
- Every HTTP request is automatically logged with:
222
- - Request method, path, status code
223
- - Request duration
224
- - Request ID for correlation
225
- - User agent, IP address
226
- - Custom fields you configure
526
+ Every request automatically logs:
227
527
 
228
- ### Custom Logging
528
+ ```json
529
+ {
530
+ "timestamp": "2024-01-15T10:30:45.123Z",
531
+ "level": "info",
532
+ "message": "HTTP Request",
533
+ "request_id": "req_1234567890abcdef",
534
+ "trace_id": "trace_abcdef1234567890",
535
+ "span_id": "span_fedcba0987654321",
536
+ "method": "POST",
537
+ "path": "/users",
538
+ "user_agent": "curl/7.68.0",
539
+ "remote_ip": "192.168.1.100",
540
+ "status": 201,
541
+ "duration_ms": 234.56,
542
+ "request_size_bytes": 156,
543
+ "response_size_bytes": 89
544
+ }
545
+ ```
229
546
 
230
- Add structured logging in your endpoints:
547
+ ### Custom Logging in Endpoints
231
548
 
232
549
  ```ruby
233
- endpoint = RapiTapir.endpoint
234
- .post
235
- .in("/users")
236
- .with_logging(level: :info, fields: [:user_id, :operation])
237
- .handle do |request|
238
- user_data = request.body
239
-
240
- # Structured info logging
241
- RapiTapir::Observability::Logging.info(
242
- "Creating user",
243
- user_email: user_data[:email],
244
- user_age: user_data[:age],
245
- operation: 'user_creation'
246
- )
550
+ endpoint(
551
+ PUT('/users/:id/profile')
552
+ .path_param(:id, T.uuid, description: 'User ID')
553
+ .body(T.hash({
554
+ "name" => T.optional(T.string),
555
+ "bio" => T.optional(T.string),
556
+ "preferences" => T.optional(T.hash({}))
557
+ }))
558
+ .tags('Users')
559
+ .ok(T.hash({
560
+ "id" => T.uuid,
561
+ "name" => T.string,
562
+ "updated_at" => T.datetime
563
+ }))
564
+ .build
565
+ ) do |inputs|
566
+ user_id = inputs[:id]
567
+ updates = inputs[:body]
568
+
569
+ # Structured info logging
570
+ log_info('Profile update started', {
571
+ user_id: user_id,
572
+ fields_to_update: updates.keys,
573
+ request_size: updates.to_json.bytesize
574
+ })
575
+
576
+ begin
577
+ user = User.find(user_id)
247
578
 
248
- # Log with different levels
249
- RapiTapir::Observability::Logging.debug(
250
- "Validation passed",
251
- validation_time_ms: 5.2
252
- )
579
+ # Log user context
580
+ log_debug('User found', {
581
+ user_id: user.id,
582
+ user_email: user.email,
583
+ last_updated: user.updated_at
584
+ })
253
585
 
254
- RapiTapir::Observability::Logging.warn(
255
- "Slow database response",
256
- db_response_time_ms: 1200
257
- )
586
+ # Validate and apply updates
587
+ original_values = {}
588
+ updates.each do |field, value|
589
+ original_values[field] = user.send(field)
590
+ user.send("#{field}=", value)
591
+ end
258
592
 
259
- # Log errors with context
260
- begin
261
- create_user(user_data)
262
- rescue => e
263
- RapiTapir::Observability::Logging.log_error(
264
- e,
265
- user_email: user_data[:email],
266
- operation: 'user_creation',
267
- request_id: request.id
268
- )
269
- raise
593
+ if user.valid?
594
+ user.save!
595
+
596
+ # Log successful update
597
+ log_info('Profile updated successfully', {
598
+ user_id: user_id,
599
+ updated_fields: updates.keys,
600
+ original_values: original_values,
601
+ new_values: updates
602
+ })
603
+
604
+ user.to_h
605
+ else
606
+ # Log validation errors
607
+ log_warn('Profile update validation failed', {
608
+ user_id: user_id,
609
+ validation_errors: user.errors.full_messages,
610
+ attempted_updates: updates
611
+ })
612
+
613
+ halt 422, {
614
+ error: 'Validation failed',
615
+ details: user.errors.full_messages
616
+ }.to_json
270
617
  end
271
618
 
272
- # Your logic here
619
+ rescue ActiveRecord::RecordNotFound => e
620
+ log_warn('User not found for profile update', {
621
+ user_id: user_id,
622
+ error: e.message
623
+ })
624
+
625
+ halt 404, { error: 'User not found' }.to_json
626
+
627
+ rescue => e
628
+ log_error('Profile update failed', {
629
+ user_id: user_id,
630
+ error_class: e.class.name,
631
+ error_message: e.message,
632
+ backtrace: e.backtrace.first(10)
633
+ })
634
+
635
+ halt 500, { error: 'Internal server error' }.to_json
273
636
  end
637
+ end
274
638
  ```
275
639
 
276
- ### Log Formats
640
+ ### Correlation IDs
277
641
 
278
- Choose from multiple log formats:
642
+ Request correlation is automatically handled:
279
643
 
280
644
  ```ruby
281
- # JSON format (default for structured logging)
282
- {"timestamp":"2024-01-01T12:00:00Z","level":"INFO","message":"User created","user_id":"123"}
283
-
284
- # Logfmt format
285
- timestamp=2024-01-01T12:00:00Z level=INFO message="User created" user_id=123
286
-
287
- # Text format
288
- 2024-01-01 12:00:00 [INFO] User created user_id=123
645
+ # Access correlation IDs in endpoints
646
+ endpoint(
647
+ GET('/debug/request-info')
648
+ .summary('Get current request debugging info')
649
+ .tags('Debug')
650
+ .ok(T.hash({
651
+ "request_id" => T.string,
652
+ "trace_id" => T.string,
653
+ "span_id" => T.string,
654
+ "user_id" => T.optional(T.string)
655
+ }))
656
+ .build
657
+ ) do |inputs|
658
+ {
659
+ request_id: current_request_id,
660
+ trace_id: current_trace_id,
661
+ span_id: current_span_id,
662
+ user_id: current_user&.id
663
+ }
664
+ end
289
665
  ```
290
666
 
291
667
  ## Health Checks
292
668
 
293
- RapiTapir provides comprehensive health check functionality:
294
-
295
- ### Default Health Checks
296
-
297
- - `ruby_runtime` - Ruby runtime status
298
- - `memory_usage` - Memory and GC statistics
299
- - `thread_count` - Active thread count
300
-
301
- ### Custom Health Checks
302
-
303
- Add custom health checks for your dependencies:
669
+ ### Built-in Health Checks
304
670
 
305
671
  ```ruby
306
- RapiTapir.configure do |config|
307
- config.health_check.enable
308
-
309
- # Database health check
310
- config.health_check.add_check(:database) do
311
- begin
312
- result = ActiveRecord::Base.connection.execute("SELECT 1")
313
- { status: :healthy, message: "Database connection OK" }
314
- rescue => e
315
- { status: :unhealthy, message: "Database error: #{e.message}" }
316
- end
317
- end
318
-
319
- # Redis health check with timeout
320
- config.health_check.add_check(:redis) do
321
- begin
322
- Timeout.timeout(5) do
323
- Redis.current.ping
324
- { status: :healthy, message: "Redis connection OK" }
672
+ class HealthyAPI < SinatraRapiTapir
673
+ rapitapir do
674
+ enable_health_checks do |health|
675
+ health.endpoint = '/health'
676
+ health.detailed_endpoint = '/health/detailed'
677
+
678
+ # Add custom health checks
679
+ health.add_check(:database) do
680
+ begin
681
+ ActiveRecord::Base.connection.execute('SELECT 1')
682
+ { status: :healthy, message: 'Database connection OK' }
683
+ rescue => e
684
+ { status: :unhealthy, message: "Database error: #{e.message}" }
685
+ end
325
686
  end
326
- rescue Timeout::Error
327
- { status: :unhealthy, message: "Redis timeout" }
328
- rescue => e
329
- { status: :unhealthy, message: "Redis error: #{e.message}" }
330
- end
331
- end
332
-
333
- # External API health check
334
- config.health_check.add_check(:payment_api) do
335
- begin
336
- response = HTTP.timeout(10).get("https://api.stripe.com/v1/charges")
337
- if response.status.success?
338
- { status: :healthy, message: "Payment API reachable" }
339
- else
340
- { status: :unhealthy, message: "Payment API returned #{response.status}" }
687
+
688
+ health.add_check(:redis) do
689
+ begin
690
+ Redis.current.ping
691
+ { status: :healthy, message: 'Redis connection OK' }
692
+ rescue => e
693
+ { status: :unhealthy, message: "Redis error: #{e.message}" }
694
+ end
695
+ end
696
+
697
+ health.add_check(:external_api) do
698
+ begin
699
+ response = HTTP.timeout(5).get("#{ENV['EXTERNAL_API_URL']}/health")
700
+ if response.status.success?
701
+ { status: :healthy, message: 'External API responding' }
702
+ else
703
+ { status: :unhealthy, message: "External API returned #{response.status}" }
704
+ end
705
+ rescue => e
706
+ { status: :unhealthy, message: "External API error: #{e.message}" }
707
+ end
341
708
  end
342
- rescue => e
343
- { status: :unhealthy, message: "Payment API unreachable: #{e.message}" }
344
709
  end
345
710
  end
346
711
  end
347
712
  ```
348
713
 
349
- ### Health Check Endpoints
350
-
351
- Health checks are available at multiple endpoints:
714
+ Health check endpoints respond with:
352
715
 
353
716
  ```bash
354
- # Overall health status
355
- GET /health
717
+ # Basic health check
718
+ curl http://localhost:4567/health
719
+ # Response: {"status":"healthy","timestamp":"2024-01-15T10:30:45Z"}
720
+
721
+ # Detailed health check
722
+ curl http://localhost:4567/health/detailed
723
+ ```
724
+
725
+ ```json
356
726
  {
357
727
  "status": "healthy",
358
- "timestamp": "2024-01-01T12:00:00Z",
359
- "service": "rapitapir",
360
- "version": "0.1.0",
361
- "checks": [
362
- {
363
- "name": "database",
728
+ "timestamp": "2024-01-15T10:30:45Z",
729
+ "checks": {
730
+ "database": {
364
731
  "status": "healthy",
365
732
  "message": "Database connection OK",
366
- "duration_ms": 2.5
733
+ "duration_ms": 12.34
734
+ },
735
+ "redis": {
736
+ "status": "healthy",
737
+ "message": "Redis connection OK",
738
+ "duration_ms": 5.67
739
+ },
740
+ "external_api": {
741
+ "status": "unhealthy",
742
+ "message": "External API error: Connection timeout",
743
+ "duration_ms": 5000.0
367
744
  }
368
- ]
369
- }
370
-
371
- # Individual health check
372
- GET /health/check?name=database
373
- {
374
- "name": "database",
375
- "status": "healthy",
376
- "message": "Database connection OK",
377
- "duration_ms": 2.5
378
- }
379
-
380
- # List available checks
381
- GET /health/checks
382
- {
383
- "available_checks": [
384
- {"name": "ruby_runtime", "url": "/health/check?name=ruby_runtime"},
385
- {"name": "database", "url": "/health/check?name=database"}
386
- ],
387
- "total": 2
745
+ },
746
+ "system": {
747
+ "uptime_seconds": 86400,
748
+ "memory_usage_mb": 125.6,
749
+ "load_average": [0.1, 0.2, 0.15]
750
+ }
388
751
  }
389
752
  ```
390
753
 
391
- ## Middleware Integration
754
+ ## Integration Examples
392
755
 
393
- ### Rack Applications
394
-
395
- Use the observability middleware with any Rack application:
756
+ ### Kubernetes Integration
396
757
 
397
758
  ```ruby
398
- require 'rack'
399
- require 'rapitapir'
400
-
401
- # Configure observability
402
- RapiTapir.configure do |config|
403
- config.metrics.enable_prometheus
404
- config.tracing.enable_opentelemetry
405
- config.logging.enable_structured
406
- config.health_check.enable
407
- end
408
-
409
- # Build application with observability
410
- app = Rack::Builder.new do
411
- # Add observability middleware (includes metrics, tracing, logging)
412
- use RapiTapir::Observability::RackMiddleware
413
-
414
- # Your application
415
- run MyApp.new
759
+ class KubernetesAPI < SinatraRapiTapir
760
+ rapitapir do
761
+ enable_observability do |obs|
762
+ # Kubernetes-friendly configuration
763
+ obs.metrics.endpoint = '/metrics'
764
+ obs.health_checks.endpoint = '/health'
765
+ obs.health_checks.readiness_endpoint = '/ready'
766
+ obs.health_checks.liveness_endpoint = '/live'
767
+
768
+ obs.logging.format = :json
769
+ obs.logging.include_kubernetes_metadata = true
770
+ end
771
+ end
416
772
  end
417
-
418
- run app
419
773
  ```
420
774
 
421
- ### Sinatra Integration
775
+ ### Honeycomb Integration
422
776
 
423
777
  ```ruby
424
- require 'sinatra'
425
- require 'rapitapir'
426
-
427
- # Configure observability
428
- RapiTapir.configure do |config|
429
- config.metrics.enable_prometheus
430
- config.tracing.enable_opentelemetry
431
- config.logging.enable_structured
432
- config.health_check.enable
433
- end
434
-
435
- class MyApp < Sinatra::Base
436
- use RapiTapir::Observability::RackMiddleware
437
-
438
- get '/users' do
439
- # Your route logic
778
+ class HoneycombAPI < SinatraRapiTapir
779
+ rapitapir do
780
+ enable_tracing do |tracing|
781
+ tracing.provider = :opentelemetry
782
+ tracing.exporter = :honeycomb
783
+ tracing.honeycomb_api_key = ENV['HONEYCOMB_API_KEY']
784
+ tracing.honeycomb_dataset = 'user-api'
785
+ tracing.sample_rate = 0.1
786
+ end
440
787
  end
441
788
  end
442
789
  ```
443
790
 
444
- ### Rails Integration
791
+ ### DataDog Integration
445
792
 
446
793
  ```ruby
447
- # config/application.rb
448
- require 'rapitapir'
449
-
450
- class Application < Rails::Application
451
- # Configure observability
452
- config.before_configuration do
453
- RapiTapir.configure do |config|
454
- config.metrics.enable_prometheus(
455
- namespace: 'rails_app',
456
- labels: { environment: Rails.env }
457
- )
458
- config.tracing.enable_opentelemetry(
459
- service_name: 'my-rails-app',
460
- service_version: MyApp::VERSION
461
- )
462
- config.logging.enable_structured(level: :info)
463
- config.health_check.enable
794
+ class DataDogAPI < SinatraRapiTapir
795
+ rapitapir do
796
+ enable_observability do |obs|
797
+ obs.metrics do |metrics|
798
+ metrics.provider = :datadog
799
+ metrics.datadog_api_key = ENV['DATADOG_API_KEY']
800
+ metrics.tags = {
801
+ env: ENV['RACK_ENV'],
802
+ service: 'user-api',
803
+ version: ENV['APP_VERSION']
804
+ }
805
+ end
806
+
807
+ obs.tracing do |tracing|
808
+ tracing.provider = :datadog
809
+ tracing.service_name = 'user-api'
810
+ tracing.environment = ENV['RACK_ENV']
811
+ end
464
812
  end
465
813
  end
466
-
467
- # Add observability middleware
468
- config.middleware.use RapiTapir::Observability::RackMiddleware
469
814
  end
470
815
  ```
471
816
 
472
- ## Examples
817
+ ## Production Setup
473
818
 
474
- ### Basic E-commerce API
819
+ ### Docker Configuration
475
820
 
476
- ```ruby
477
- require 'rapitapir'
478
-
479
- # Configure observability
480
- RapiTapir.configure do |config|
481
- config.metrics.enable_prometheus(namespace: 'ecommerce')
482
- config.tracing.enable_opentelemetry(service_name: 'ecommerce-api')
483
- config.logging.enable_structured
484
- config.health_check.enable
485
- end
821
+ ```dockerfile
822
+ # Dockerfile
823
+ FROM ruby:3.2-alpine
486
824
 
487
- # Create order endpoint
488
- create_order = RapiTapir.endpoint
489
- .post
490
- .in("/orders")
491
- .json_body({
492
- customer_id: :uuid,
493
- items: [{ product_id: :uuid, quantity: :integer, price: :float }]
494
- })
495
- .out_json({ id: :uuid, status: :string, total: :float })
496
- .with_metrics("order_creation")
497
- .with_tracing
498
- .with_logging(fields: [:customer_id, :order_total, :item_count])
499
- .handle do |request|
500
- order_data = request.body
501
-
502
- # Add business context to tracing
503
- RapiTapir::Observability::Tracing.set_attribute('customer.id', order_data[:customer_id])
504
- RapiTapir::Observability::Tracing.set_attribute('order.item_count', order_data[:items].length)
505
-
506
- total = order_data[:items].sum { |item| item[:quantity] * item[:price] }
507
- RapiTapir::Observability::Tracing.set_attribute('order.total', total)
508
-
509
- # Structured logging
510
- RapiTapir::Observability::Logging.info(
511
- "Processing order",
512
- customer_id: order_data[:customer_id],
513
- order_total: total,
514
- item_count: order_data[:items].length
515
- )
516
-
517
- # Process order with nested tracing
518
- order_id = RapiTapir::Observability::Tracing.start_span("create_order_record") do
519
- SecureRandom.uuid
520
- end
521
-
522
- RapiTapir::Observability::Tracing.start_span("send_confirmation_email") do |span|
523
- span.set_attribute('email.type', 'order_confirmation')
524
- # Send confirmation email
525
- end
526
-
527
- {
528
- id: order_id,
529
- status: 'confirmed',
530
- total: total
531
- }
532
- end
533
- ```
825
+ # Install dependencies
826
+ RUN apk add --no-cache build-base
534
827
 
535
- ### Advanced Monitoring Setup
828
+ WORKDIR /app
829
+ COPY Gemfile* ./
830
+ RUN bundle install
536
831
 
537
- ```ruby
538
- # Production observability configuration
539
- RapiTapir.configure do |config|
540
- # Comprehensive metrics
541
- config.metrics.enable_prometheus(
542
- namespace: 'production_api',
543
- labels: {
544
- service: ENV['SERVICE_NAME'],
545
- version: ENV['APP_VERSION'],
546
- environment: ENV['RAILS_ENV'],
547
- datacenter: ENV['DATACENTER']
548
- }
549
- )
550
-
551
- # Distributed tracing
552
- config.tracing.enable_opentelemetry(
553
- service_name: ENV['SERVICE_NAME'],
554
- service_version: ENV['APP_VERSION']
555
- )
556
-
557
- # Structured logging for log aggregation
558
- config.logging.enable_structured(
559
- level: ENV.fetch('LOG_LEVEL', 'info').to_sym,
560
- fields: [
561
- :timestamp, :level, :message, :request_id, :trace_id,
562
- :method, :path, :status, :duration,
563
- :user_id, :tenant_id, :session_id,
564
- :source_ip, :user_agent
565
- ]
566
- )
567
-
568
- # Comprehensive health checks
569
- config.health_check.enable(endpoint: '/health')
570
-
571
- # Database health check
572
- config.health_check.add_check(:database) do
573
- ActiveRecord::Base.connection.execute("SELECT 1")
574
- { status: :healthy, message: "Primary database OK" }
575
- rescue => e
576
- { status: :unhealthy, message: "Database error: #{e.message}" }
577
- end
578
-
579
- # Redis health check
580
- config.health_check.add_check(:redis) do
581
- Redis.current.ping
582
- { status: :healthy, message: "Redis cache OK" }
583
- rescue => e
584
- { status: :unhealthy, message: "Redis error: #{e.message}" }
585
- end
586
-
587
- # Message queue health check
588
- config.health_check.add_check(:message_queue) do
589
- # Check Sidekiq or similar
590
- if defined?(Sidekiq)
591
- stats = Sidekiq::Stats.new
592
- queue_size = stats.enqueued
593
-
594
- if queue_size > 10000
595
- { status: :warning, message: "High queue size: #{queue_size}" }
596
- else
597
- { status: :healthy, message: "Queue size: #{queue_size}" }
598
- end
599
- else
600
- { status: :healthy, message: "No message queue configured" }
601
- end
602
- rescue => e
603
- { status: :unhealthy, message: "Queue error: #{e.message}" }
604
- end
605
- end
606
- ```
832
+ COPY . .
607
833
 
608
- ## Best Practices
834
+ # Expose metrics and health check ports
835
+ EXPOSE 4567 9090
609
836
 
610
- ### 1. Metric Naming
837
+ # Health check
838
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
839
+ CMD curl -f http://localhost:4567/health || exit 1
611
840
 
612
- Use consistent metric naming:
613
- - Use underscores for separating words
614
- - Include units in metric names (e.g., `_seconds`, `_bytes`)
615
- - Use clear, descriptive names
841
+ CMD ["bundle", "exec", "ruby", "app.rb"]
842
+ ```
616
843
 
617
- ### 2. Trace Context
844
+ ### Docker Compose with Observability Stack
845
+
846
+ ```yaml
847
+ # docker-compose.yml
848
+ version: '3.8'
849
+
850
+ services:
851
+ api:
852
+ build: .
853
+ ports:
854
+ - "4567:4567"
855
+ environment:
856
+ - RACK_ENV=production
857
+ - JAEGER_ENDPOINT=http://jaeger:14268/api/traces
858
+ - PROMETHEUS_PUSHGATEWAY=prometheus-pushgateway:9091
859
+ depends_on:
860
+ - jaeger
861
+ - prometheus
862
+
863
+ prometheus:
864
+ image: prom/prometheus:latest
865
+ ports:
866
+ - "9090:9090"
867
+ volumes:
868
+ - ./prometheus.yml:/etc/prometheus/prometheus.yml
869
+ command:
870
+ - '--config.file=/etc/prometheus/prometheus.yml'
871
+ - '--storage.tsdb.path=/prometheus'
872
+
873
+ jaeger:
874
+ image: jaegertracing/all-in-one:latest
875
+ ports:
876
+ - "16686:16686"
877
+ - "14268:14268"
878
+ environment:
879
+ - COLLECTOR_OTLP_ENABLED=true
880
+
881
+ grafana:
882
+ image: grafana/grafana:latest
883
+ ports:
884
+ - "3000:3000"
885
+ environment:
886
+ - GF_SECURITY_ADMIN_PASSWORD=admin
887
+ volumes:
888
+ - grafana-data:/var/lib/grafana
889
+
890
+ volumes:
891
+ grafana-data:
892
+ ```
618
893
 
619
- Add meaningful attributes to traces:
620
- - Business identifiers (user_id, order_id, etc.)
621
- - Request context (tenant_id, api_version)
622
- - Performance indicators (cache_hit, db_query_count)
894
+ ### Prometheus Configuration
623
895
 
624
- ### 3. Structured Logging
896
+ ```yaml
897
+ # prometheus.yml
898
+ global:
899
+ scrape_interval: 15s
625
900
 
626
- Design your log structure:
627
- - Use consistent field names across services
628
- - Include correlation IDs for request tracing
629
- - Log at appropriate levels (debug for development, info+ for production)
901
+ scrape_configs:
902
+ - job_name: 'rapitapir-api'
903
+ static_configs:
904
+ - targets: ['api:4567']
905
+ metrics_path: '/metrics'
906
+ scrape_interval: 5s
630
907
 
631
- ### 4. Health Check Design
908
+ - job_name: 'node-exporter'
909
+ static_configs:
910
+ - targets: ['node-exporter:9100']
911
+ ```
632
912
 
633
- Create meaningful health checks:
634
- - Test actual functionality, not just connectivity
635
- - Include response time thresholds
636
- - Use timeouts to prevent hanging checks
637
- - Return actionable status messages
913
+ ### Grafana Dashboard
638
914
 
639
- ### 5. Error Handling
915
+ ```json
916
+ {
917
+ "dashboard": {
918
+ "title": "RapiTapir API Metrics",
919
+ "panels": [
920
+ {
921
+ "title": "Request Rate",
922
+ "type": "graph",
923
+ "targets": [
924
+ {
925
+ "expr": "rate(http_requests_total[5m])",
926
+ "legendFormat": "{{method}} {{path}}"
927
+ }
928
+ ]
929
+ },
930
+ {
931
+ "title": "Response Time",
932
+ "type": "graph",
933
+ "targets": [
934
+ {
935
+ "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))",
936
+ "legendFormat": "95th percentile"
937
+ }
938
+ ]
939
+ },
940
+ {
941
+ "title": "Error Rate",
942
+ "type": "graph",
943
+ "targets": [
944
+ {
945
+ "expr": "rate(http_requests_total{status=~\"5..\"}[5m])",
946
+ "legendFormat": "5xx errors"
947
+ }
948
+ ]
949
+ }
950
+ ]
951
+ }
952
+ }
953
+ ```
640
954
 
641
- Implement comprehensive error observability:
642
- - Always record exceptions in traces
643
- - Log errors with sufficient context
644
- - Use error metrics to track error rates
645
- - Include error classification (validation, system, external)
955
+ ---
646
956
 
647
- This observability implementation provides production-ready monitoring capabilities for RapiTapir applications, enabling comprehensive visibility into system performance, health, and behavior.
957
+ This guide provides comprehensive coverage of RapiTapir's observability features. For more examples, see the [observability examples](../examples/observability/) and the [production setup guide](../examples/production_ready_example.rb).