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.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -7
- data/.rubocop_todo.yml +83 -0
- data/README.md +1319 -235
- data/RUBY_WEEKLY_LAUNCH_POST.md +219 -0
- data/docs/RAILS_INTEGRATION_IMPLEMENTATION.md +209 -0
- data/docs/SINATRA_EXTENSION.md +399 -348
- data/docs/STRICT_VALIDATION.md +229 -0
- data/docs/VALIDATION_IMPROVEMENTS.md +218 -0
- data/docs/ai-integration-plan.md +112 -0
- data/docs/auto-derivation.md +505 -92
- data/docs/endpoint-definition.md +536 -129
- data/docs/n8n-integration.md +212 -0
- data/docs/observability.md +810 -500
- data/docs/using-mcp.md +93 -0
- data/examples/ai/knowledge_base_rag.rb +83 -0
- data/examples/ai/user_management_mcp.rb +92 -0
- data/examples/ai/user_validation_llm.rb +187 -0
- data/examples/rails/RAILS_8_GUIDE.md +165 -0
- data/examples/rails/RAILS_LOADING_FIX.rb +35 -0
- data/examples/rails/README.md +497 -0
- data/examples/rails/comprehensive_test.rb +91 -0
- data/examples/rails/config/routes.rb +48 -0
- data/examples/rails/debug_controller.rb +63 -0
- data/examples/rails/detailed_test.rb +46 -0
- data/examples/rails/enhanced_users_controller.rb +278 -0
- data/examples/rails/final_server_test.rb +50 -0
- data/examples/rails/hello_world_app.rb +116 -0
- data/examples/rails/hello_world_controller.rb +186 -0
- data/examples/rails/hello_world_routes.rb +28 -0
- data/examples/rails/rails8_minimal_demo.rb +132 -0
- data/examples/rails/rails8_simple_demo.rb +140 -0
- data/examples/rails/rails8_working_demo.rb +255 -0
- data/examples/rails/real_world_blog_api.rb +510 -0
- data/examples/rails/server_test.rb +46 -0
- data/examples/rails/test_direct_processing.rb +41 -0
- data/examples/rails/test_hello_world.rb +80 -0
- data/examples/rails/test_rails_integration.rb +54 -0
- data/examples/rails/traditional_app/Gemfile +37 -0
- data/examples/rails/traditional_app/README.md +265 -0
- data/examples/rails/traditional_app/app/controllers/api/v1/posts_controller.rb +254 -0
- data/examples/rails/traditional_app/app/controllers/api/v1/users_controller.rb +220 -0
- data/examples/rails/traditional_app/app/controllers/application_controller.rb +86 -0
- data/examples/rails/traditional_app/app/controllers/application_controller_simplified.rb +87 -0
- data/examples/rails/traditional_app/app/controllers/documentation_controller.rb +149 -0
- data/examples/rails/traditional_app/app/controllers/health_controller.rb +42 -0
- data/examples/rails/traditional_app/config/routes.rb +25 -0
- data/examples/rails/traditional_app/config/routes_best_practice.rb +25 -0
- data/examples/rails/traditional_app/config/routes_simplified.rb +36 -0
- data/examples/rails/traditional_app_runnable.rb +406 -0
- data/examples/rails/users_controller.rb +4 -1
- data/examples/serverless/Gemfile +43 -0
- data/examples/serverless/QUICKSTART.md +331 -0
- data/examples/serverless/README.md +520 -0
- data/examples/serverless/aws_lambda_example.rb +307 -0
- data/examples/serverless/aws_sam_template.yaml +215 -0
- data/examples/serverless/azure_functions_example.rb +407 -0
- data/examples/serverless/deploy.rb +204 -0
- data/examples/serverless/gcp_cloud_functions_example.rb +367 -0
- data/examples/serverless/gcp_function.yaml +23 -0
- data/examples/serverless/host.json +24 -0
- data/examples/serverless/package.json +32 -0
- data/examples/serverless/spec/aws_lambda_spec.rb +196 -0
- data/examples/serverless/spec/spec_helper.rb +89 -0
- data/examples/serverless/vercel.json +31 -0
- data/examples/serverless/vercel_example.rb +404 -0
- data/examples/strict_validation_examples.rb +104 -0
- data/examples/validation_error_examples.rb +173 -0
- data/lib/rapitapir/ai/llm_instruction.rb +456 -0
- data/lib/rapitapir/ai/mcp.rb +134 -0
- data/lib/rapitapir/ai/rag.rb +287 -0
- data/lib/rapitapir/ai/rag_middleware.rb +147 -0
- data/lib/rapitapir/auth/oauth2.rb +43 -57
- data/lib/rapitapir/cli/command.rb +362 -2
- data/lib/rapitapir/cli/mcp_export.rb +18 -0
- data/lib/rapitapir/cli/validator.rb +2 -6
- data/lib/rapitapir/core/endpoint.rb +59 -6
- data/lib/rapitapir/core/enhanced_endpoint.rb +2 -6
- data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +53 -0
- data/lib/rapitapir/endpoint_registry.rb +47 -0
- data/lib/rapitapir/observability/health_check.rb +4 -4
- data/lib/rapitapir/observability/logging.rb +10 -10
- data/lib/rapitapir/schema.rb +2 -2
- data/lib/rapitapir/server/rack_adapter.rb +1 -3
- data/lib/rapitapir/server/rails/configuration.rb +77 -0
- data/lib/rapitapir/server/rails/controller_base.rb +185 -0
- data/lib/rapitapir/server/rails/documentation_helpers.rb +76 -0
- data/lib/rapitapir/server/rails/resource_builder.rb +181 -0
- data/lib/rapitapir/server/rails/routes.rb +114 -0
- data/lib/rapitapir/server/rails_adapter.rb +10 -3
- data/lib/rapitapir/server/rails_adapter_class.rb +1 -3
- data/lib/rapitapir/server/rails_controller.rb +1 -3
- data/lib/rapitapir/server/rails_integration.rb +67 -0
- data/lib/rapitapir/server/rails_response_handler.rb +16 -3
- data/lib/rapitapir/server/sinatra_adapter.rb +29 -5
- data/lib/rapitapir/server/sinatra_integration.rb +4 -4
- data/lib/rapitapir/sinatra/extension.rb +2 -2
- data/lib/rapitapir/sinatra/oauth2_helpers.rb +34 -40
- data/lib/rapitapir/types/array.rb +4 -0
- data/lib/rapitapir/types/auto_derivation.rb +4 -18
- data/lib/rapitapir/types/datetime.rb +1 -3
- data/lib/rapitapir/types/float.rb +2 -6
- data/lib/rapitapir/types/hash.rb +40 -2
- data/lib/rapitapir/types/integer.rb +4 -12
- data/lib/rapitapir/types/object.rb +6 -2
- data/lib/rapitapir/types.rb +6 -2
- data/lib/rapitapir/version.rb +1 -1
- data/lib/rapitapir.rb +5 -3
- data/rapitapir.gemspec +7 -5
- metadata +116 -16
data/docs/observability.md
CHANGED
@@ -1,647 +1,957 @@
|
|
1
1
|
# RapiTapir Observability Guide
|
2
2
|
|
3
|
-
This guide covers
|
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
|
-
- [
|
14
|
-
- [
|
14
|
+
- [Integration Examples](#integration-examples)
|
15
|
+
- [Production Setup](#production-setup)
|
15
16
|
|
16
|
-
##
|
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
|
-
|
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
|
-
|
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
|
-
|
27
|
+
## Quick Start
|
53
28
|
|
54
29
|
```ruby
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
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
|
-
|
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
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
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
|
-
|
191
|
+
## Configuration
|
192
|
+
|
193
|
+
### Global Configuration
|
79
194
|
|
80
195
|
```ruby
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
###
|
251
|
+
### Environment-Specific Configuration
|
94
252
|
|
95
253
|
```ruby
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
290
|
+
### Built-in Metrics
|
115
291
|
|
116
|
-
|
292
|
+
RapiTapir automatically collects these metrics:
|
117
293
|
|
118
|
-
- `
|
119
|
-
- `
|
120
|
-
- `
|
121
|
-
- `
|
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
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
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
|
346
|
+
Metrics are automatically exposed at `/metrics` endpoint:
|
154
347
|
|
155
348
|
```bash
|
156
|
-
|
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
|
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
|
-
|
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
|
-
|
370
|
+
### Custom Tracing in Endpoints
|
174
371
|
|
175
372
|
```ruby
|
176
|
-
endpoint
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
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
|
-
|
186
|
-
|
187
|
-
span.
|
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
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
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
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
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
|
-
|
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
|
-
|
208
|
-
|
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
|
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
|
-
|
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
|
-
|
547
|
+
### Custom Logging in Endpoints
|
231
548
|
|
232
549
|
```ruby
|
233
|
-
endpoint
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
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
|
249
|
-
|
250
|
-
|
251
|
-
|
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
|
-
|
255
|
-
|
256
|
-
|
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
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
)
|
269
|
-
|
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
|
-
|
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
|
-
###
|
640
|
+
### Correlation IDs
|
277
641
|
|
278
|
-
|
642
|
+
Request correlation is automatically handled:
|
279
643
|
|
280
644
|
```ruby
|
281
|
-
#
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
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
|
-
|
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
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
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
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
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
|
-
|
350
|
-
|
351
|
-
Health checks are available at multiple endpoints:
|
714
|
+
Health check endpoints respond with:
|
352
715
|
|
353
716
|
```bash
|
354
|
-
#
|
355
|
-
|
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-
|
359
|
-
"
|
360
|
-
|
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":
|
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
|
-
|
372
|
-
|
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
|
-
##
|
754
|
+
## Integration Examples
|
392
755
|
|
393
|
-
###
|
394
|
-
|
395
|
-
Use the observability middleware with any Rack application:
|
756
|
+
### Kubernetes Integration
|
396
757
|
|
397
758
|
```ruby
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
#
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
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
|
-
###
|
775
|
+
### Honeycomb Integration
|
422
776
|
|
423
777
|
```ruby
|
424
|
-
|
425
|
-
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
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
|
-
###
|
791
|
+
### DataDog Integration
|
445
792
|
|
446
793
|
```ruby
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
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
|
-
##
|
817
|
+
## Production Setup
|
473
818
|
|
474
|
-
###
|
819
|
+
### Docker Configuration
|
475
820
|
|
476
|
-
```
|
477
|
-
|
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
|
-
#
|
488
|
-
|
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
|
-
|
828
|
+
WORKDIR /app
|
829
|
+
COPY Gemfile* ./
|
830
|
+
RUN bundle install
|
536
831
|
|
537
|
-
|
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
|
-
|
834
|
+
# Expose metrics and health check ports
|
835
|
+
EXPOSE 4567 9090
|
609
836
|
|
610
|
-
|
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
|
-
|
613
|
-
|
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
|
-
###
|
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
|
-
|
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
|
-
|
896
|
+
```yaml
|
897
|
+
# prometheus.yml
|
898
|
+
global:
|
899
|
+
scrape_interval: 15s
|
625
900
|
|
626
|
-
|
627
|
-
-
|
628
|
-
|
629
|
-
-
|
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
|
-
|
908
|
+
- job_name: 'node-exporter'
|
909
|
+
static_configs:
|
910
|
+
- targets: ['node-exporter:9100']
|
911
|
+
```
|
632
912
|
|
633
|
-
|
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
|
-
|
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
|
-
|
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
|
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).
|