rapitapir 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +57 -0
- data/CHANGELOG.md +94 -0
- data/CLEANUP_SUMMARY.md +155 -0
- data/CONTRIBUTING.md +280 -0
- data/LICENSE +21 -0
- data/README.md +485 -0
- data/debug_hash.rb +20 -0
- data/docs/EXTENSION_COMPARISON.md +388 -0
- data/docs/SINATRA_EXTENSION.md +467 -0
- data/docs/archive/PHASE_1_2_COMPLETE.md +77 -0
- data/docs/archive/PHASE_1_3_COMPLETE.md +152 -0
- data/docs/archive/PHASE_2_1_OBSERVABILITY_COMPLETED.md +203 -0
- data/docs/archive/PHASE_2_SUMMARY.md +209 -0
- data/docs/archive/REFACTORING_SUMMARY.md +184 -0
- data/docs/archive/phase_1_3_plan.md +136 -0
- data/docs/archive/sinatra_extension_summary.md +188 -0
- data/docs/archive/sinatra_working_solution.md +113 -0
- data/docs/archive/typescript-client-generator-summary.md +259 -0
- data/docs/auto-derivation.md +146 -0
- data/docs/blueprint.md +1091 -0
- data/docs/endpoint-definition.md +211 -0
- data/docs/github_pages_fix.md +52 -0
- data/docs/github_pages_setup.md +49 -0
- data/docs/implementation-status.md +357 -0
- data/docs/observability.md +647 -0
- data/docs/phase3-plan.md +108 -0
- data/docs/sinatra_rapitapir.md +87 -0
- data/docs/type_shortcuts.md +146 -0
- data/examples/README_ENTERPRISE.md +202 -0
- data/examples/authentication_example.rb +192 -0
- data/examples/auto_derivation_ruby_friendly.rb +163 -0
- data/examples/cli/user_api_endpoints.rb +56 -0
- data/examples/client/typescript_client_example.rb +102 -0
- data/examples/client/user-api-client.ts +193 -0
- data/examples/demo_api.rb +41 -0
- data/examples/docs/documentation_example.rb +112 -0
- data/examples/docs/user-api-docs.html +789 -0
- data/examples/docs/user-api-docs.md +403 -0
- data/examples/enhanced_auto_derivation_test.rb +83 -0
- data/examples/enterprise_extension_demo.rb +417 -0
- data/examples/enterprise_rapitapir_api.rb +662 -0
- data/examples/getting_started_extension.rb +218 -0
- data/examples/hello_world.rb +74 -0
- data/examples/oauth2/.env.example +19 -0
- data/examples/oauth2/README.md +205 -0
- data/examples/oauth2/generic_oauth2_api.rb +226 -0
- data/examples/oauth2/get_token.rb +72 -0
- data/examples/oauth2/songs_api_with_auth0.rb +320 -0
- data/examples/oauth2/test_api.sh +16 -0
- data/examples/oauth2/test_songs_api.sh +110 -0
- data/examples/observability/.env.example +35 -0
- data/examples/observability/README.md +230 -0
- data/examples/observability/README_HONEYCOMB.md +332 -0
- data/examples/observability/advanced_setup.rb +384 -0
- data/examples/observability/basic_setup.rb +192 -0
- data/examples/observability/complete_test.rb +121 -0
- data/examples/observability/honeycomb_example.rb +523 -0
- data/examples/observability/honeycomb_rapitapir_clean.rb +488 -0
- data/examples/observability/honeycomb_rapitapir_example.rb +523 -0
- data/examples/observability/honeycomb_working_example.rb +489 -0
- data/examples/observability/quick_test.rb +78 -0
- data/examples/observability/simple_test.rb +14 -0
- data/examples/observability/test_honeycomb_demo.rb +354 -0
- data/examples/observability/test_live_honeycomb.rb +111 -0
- data/examples/observability/test_validation.rb +78 -0
- data/examples/observability/test_working_validation.rb +66 -0
- data/examples/openapi/user_api_schema.rb +132 -0
- data/examples/production_ready_example.rb +105 -0
- data/examples/rails/users_controller.rb +146 -0
- data/examples/readme/basic_sinatra_example.rb +128 -0
- data/examples/server/user_api.rb +179 -0
- data/examples/simple_auto_derivation_demo.rb +44 -0
- data/examples/simple_demo_api.rb +18 -0
- data/examples/sinatra/user_app.rb +127 -0
- data/examples/t_shortcut_demo.rb +59 -0
- data/examples/user_api.rb +190 -0
- data/examples/working_getting_started.rb +184 -0
- data/examples/working_simple_example.rb +195 -0
- data/lib/rapitapir/auth/configuration.rb +129 -0
- data/lib/rapitapir/auth/context.rb +122 -0
- data/lib/rapitapir/auth/errors.rb +104 -0
- data/lib/rapitapir/auth/middleware.rb +324 -0
- data/lib/rapitapir/auth/oauth2.rb +350 -0
- data/lib/rapitapir/auth/schemes.rb +420 -0
- data/lib/rapitapir/auth.rb +113 -0
- data/lib/rapitapir/cli/command.rb +535 -0
- data/lib/rapitapir/cli/server.rb +243 -0
- data/lib/rapitapir/cli/validator.rb +373 -0
- data/lib/rapitapir/client/generator_base.rb +272 -0
- data/lib/rapitapir/client/typescript_generator.rb +350 -0
- data/lib/rapitapir/core/endpoint.rb +158 -0
- data/lib/rapitapir/core/enhanced_endpoint.rb +235 -0
- data/lib/rapitapir/core/input.rb +182 -0
- data/lib/rapitapir/core/output.rb +164 -0
- data/lib/rapitapir/core/request.rb +19 -0
- data/lib/rapitapir/core/response.rb +17 -0
- data/lib/rapitapir/docs/html_generator.rb +780 -0
- data/lib/rapitapir/docs/markdown_generator.rb +464 -0
- data/lib/rapitapir/dsl/endpoint_dsl.rb +116 -0
- data/lib/rapitapir/dsl/enhanced_endpoint_dsl.rb +62 -0
- data/lib/rapitapir/dsl/enhanced_input.rb +73 -0
- data/lib/rapitapir/dsl/enhanced_output.rb +63 -0
- data/lib/rapitapir/dsl/enhanced_structures.rb +393 -0
- data/lib/rapitapir/dsl/fluent_dsl.rb +72 -0
- data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +316 -0
- data/lib/rapitapir/dsl/http_verbs.rb +77 -0
- data/lib/rapitapir/dsl/input_methods.rb +47 -0
- data/lib/rapitapir/dsl/observability_methods.rb +81 -0
- data/lib/rapitapir/dsl/output_methods.rb +43 -0
- data/lib/rapitapir/dsl/type_resolution.rb +43 -0
- data/lib/rapitapir/observability/configuration.rb +108 -0
- data/lib/rapitapir/observability/health_check.rb +236 -0
- data/lib/rapitapir/observability/logging.rb +270 -0
- data/lib/rapitapir/observability/metrics.rb +203 -0
- data/lib/rapitapir/observability/middleware.rb +243 -0
- data/lib/rapitapir/observability/tracing.rb +143 -0
- data/lib/rapitapir/observability.rb +28 -0
- data/lib/rapitapir/openapi/schema_generator.rb +403 -0
- data/lib/rapitapir/schema.rb +136 -0
- data/lib/rapitapir/server/enhanced_rack_adapter.rb +379 -0
- data/lib/rapitapir/server/middleware.rb +120 -0
- data/lib/rapitapir/server/path_matcher.rb +45 -0
- data/lib/rapitapir/server/rack_adapter.rb +215 -0
- data/lib/rapitapir/server/rails_adapter.rb +17 -0
- data/lib/rapitapir/server/rails_adapter_class.rb +53 -0
- data/lib/rapitapir/server/rails_controller.rb +72 -0
- data/lib/rapitapir/server/rails_input_processor.rb +73 -0
- data/lib/rapitapir/server/rails_response_handler.rb +29 -0
- data/lib/rapitapir/server/sinatra_adapter.rb +200 -0
- data/lib/rapitapir/server/sinatra_integration.rb +93 -0
- data/lib/rapitapir/sinatra/configuration.rb +91 -0
- data/lib/rapitapir/sinatra/extension.rb +214 -0
- data/lib/rapitapir/sinatra/oauth2_helpers.rb +236 -0
- data/lib/rapitapir/sinatra/resource_builder.rb +152 -0
- data/lib/rapitapir/sinatra/swagger_ui_generator.rb +166 -0
- data/lib/rapitapir/sinatra_rapitapir.rb +40 -0
- data/lib/rapitapir/types/array.rb +163 -0
- data/lib/rapitapir/types/auto_derivation.rb +265 -0
- data/lib/rapitapir/types/base.rb +146 -0
- data/lib/rapitapir/types/boolean.rb +46 -0
- data/lib/rapitapir/types/date.rb +92 -0
- data/lib/rapitapir/types/datetime.rb +98 -0
- data/lib/rapitapir/types/email.rb +32 -0
- data/lib/rapitapir/types/float.rb +134 -0
- data/lib/rapitapir/types/hash.rb +161 -0
- data/lib/rapitapir/types/integer.rb +143 -0
- data/lib/rapitapir/types/object.rb +156 -0
- data/lib/rapitapir/types/optional.rb +65 -0
- data/lib/rapitapir/types/string.rb +185 -0
- data/lib/rapitapir/types/uuid.rb +32 -0
- data/lib/rapitapir/types.rb +155 -0
- data/lib/rapitapir/version.rb +5 -0
- data/lib/rapitapir.rb +173 -0
- data/rapitapir.gemspec +66 -0
- metadata +387 -0
@@ -0,0 +1,384 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rapitapir'
|
4
|
+
require 'rack'
|
5
|
+
|
6
|
+
# Advanced observability configuration
|
7
|
+
RapiTapir.configure do |config|
|
8
|
+
# Comprehensive metrics setup
|
9
|
+
config.metrics.enable_prometheus(
|
10
|
+
namespace: 'ecommerce_api',
|
11
|
+
labels: {
|
12
|
+
service: 'order_service',
|
13
|
+
version: ENV.fetch('APP_VERSION', '1.0.0'),
|
14
|
+
environment: ENV.fetch('RAILS_ENV', 'development')
|
15
|
+
}
|
16
|
+
)
|
17
|
+
|
18
|
+
# Detailed tracing configuration
|
19
|
+
config.tracing.enable_opentelemetry(
|
20
|
+
service_name: 'ecommerce-order-api',
|
21
|
+
service_version: ENV.fetch('APP_VERSION', '1.0.0')
|
22
|
+
)
|
23
|
+
|
24
|
+
# Structured logging with custom fields
|
25
|
+
config.logging.enable_structured(
|
26
|
+
level: :info,
|
27
|
+
fields: %i[
|
28
|
+
timestamp level message request_id
|
29
|
+
method path status duration
|
30
|
+
user_id tenant_id trace_id span_id
|
31
|
+
]
|
32
|
+
)
|
33
|
+
|
34
|
+
# Comprehensive health checks
|
35
|
+
config.health_check.enable(endpoint: '/health')
|
36
|
+
|
37
|
+
# Database health check
|
38
|
+
config.health_check.add_check(:database) do
|
39
|
+
# Simulate database connection check
|
40
|
+
start_time = Time.now
|
41
|
+
# ActiveRecord::Base.connection.execute("SELECT 1")
|
42
|
+
duration = Time.now - start_time
|
43
|
+
|
44
|
+
{
|
45
|
+
status: :healthy,
|
46
|
+
message: 'Database connection successful',
|
47
|
+
response_time_ms: (duration * 1000).round(2)
|
48
|
+
}
|
49
|
+
rescue StandardError => e
|
50
|
+
{
|
51
|
+
status: :unhealthy,
|
52
|
+
message: "Database connection failed: #{e.message}"
|
53
|
+
}
|
54
|
+
end
|
55
|
+
|
56
|
+
# Redis health check
|
57
|
+
config.health_check.add_check(:redis) do
|
58
|
+
# Simulate Redis connection check
|
59
|
+
start_time = Time.now
|
60
|
+
# Redis.current.ping
|
61
|
+
duration = Time.now - start_time
|
62
|
+
|
63
|
+
{
|
64
|
+
status: :healthy,
|
65
|
+
message: 'Redis connection successful',
|
66
|
+
response_time_ms: (duration * 1000).round(2)
|
67
|
+
}
|
68
|
+
rescue StandardError => e
|
69
|
+
{
|
70
|
+
status: :unhealthy,
|
71
|
+
message: "Redis connection failed: #{e.message}"
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
# External API health check
|
76
|
+
config.health_check.add_check(:payment_gateway) do
|
77
|
+
# Simulate external API health check
|
78
|
+
start_time = Time.now
|
79
|
+
# HTTP.get("https://api.stripe.com/v1/charges", headers: { "Authorization" => "Bearer sk_test_..." })
|
80
|
+
duration = Time.now - start_time
|
81
|
+
|
82
|
+
if duration > 5.0 # More than 5 seconds is concerning
|
83
|
+
{
|
84
|
+
status: :warning,
|
85
|
+
message: 'Payment gateway responding slowly',
|
86
|
+
response_time_ms: (duration * 1000).round(2)
|
87
|
+
}
|
88
|
+
else
|
89
|
+
{
|
90
|
+
status: :healthy,
|
91
|
+
message: 'Payment gateway connection successful',
|
92
|
+
response_time_ms: (duration * 1000).round(2)
|
93
|
+
}
|
94
|
+
end
|
95
|
+
rescue StandardError => e
|
96
|
+
{
|
97
|
+
status: :unhealthy,
|
98
|
+
message: "Payment gateway unreachable: #{e.message}"
|
99
|
+
}
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
# Define order schema
|
104
|
+
ORDER_SCHEMA = {
|
105
|
+
customer_id: :uuid,
|
106
|
+
items: [{
|
107
|
+
product_id: :uuid,
|
108
|
+
quantity: { type: :integer, minimum: 1 },
|
109
|
+
price: { type: :float, minimum: 0.01 }
|
110
|
+
}],
|
111
|
+
shipping_address: {
|
112
|
+
street: :string,
|
113
|
+
city: :string,
|
114
|
+
state: :string,
|
115
|
+
zip_code: :string,
|
116
|
+
country: :string
|
117
|
+
},
|
118
|
+
payment_method: {
|
119
|
+
type: { type: :string, enum: %w[credit_card paypal bank_transfer] },
|
120
|
+
details: :object
|
121
|
+
}
|
122
|
+
}.freeze
|
123
|
+
|
124
|
+
# Create order endpoint with comprehensive observability
|
125
|
+
RapiTapir.endpoint
|
126
|
+
.post
|
127
|
+
.in('/orders')
|
128
|
+
.header(:authorization, Types.string(pattern: /\ABearer .+\z/), description: 'JWT token')
|
129
|
+
.header(:'x-tenant-id', :uuid, description: 'Tenant identifier')
|
130
|
+
.json_body(ORDER_SCHEMA)
|
131
|
+
.out_json({
|
132
|
+
id: :uuid,
|
133
|
+
status: { type: :string,
|
134
|
+
enum: %w[pending confirmed processing shipped delivered] },
|
135
|
+
customer_id: :uuid,
|
136
|
+
total_amount: :float,
|
137
|
+
items: [{
|
138
|
+
product_id: :uuid,
|
139
|
+
quantity: :integer,
|
140
|
+
unit_price: :float,
|
141
|
+
total_price: :float
|
142
|
+
}],
|
143
|
+
created_at: :datetime,
|
144
|
+
updated_at: :datetime
|
145
|
+
})
|
146
|
+
.with_metrics('order_creation')
|
147
|
+
.with_tracing('POST /orders')
|
148
|
+
.with_logging(
|
149
|
+
level: :info,
|
150
|
+
fields: %i[customer_id order_id total_amount item_count tenant_id]
|
151
|
+
)
|
152
|
+
.description('Create a new order')
|
153
|
+
.tag('orders')
|
154
|
+
.handle do |request|
|
155
|
+
# Extract context from headers
|
156
|
+
tenant_id = request.headers[:'x-tenant-id']
|
157
|
+
request.headers[:authorization]
|
158
|
+
|
159
|
+
# Add context to tracing
|
160
|
+
RapiTapir::Observability::Tracing.set_attribute('tenant.id', tenant_id)
|
161
|
+
RapiTapir::Observability::Tracing.set_attribute('auth.type', 'bearer')
|
162
|
+
|
163
|
+
order_data = request.body
|
164
|
+
customer_id = order_data[:customer_id]
|
165
|
+
total_amount = calculate_total_amount(order_data[:items])
|
166
|
+
item_count = order_data[:items].length
|
167
|
+
|
168
|
+
# Add business metrics to tracing
|
169
|
+
RapiTapir::Observability::Tracing.set_attribute('order.customer_id', customer_id)
|
170
|
+
RapiTapir::Observability::Tracing.set_attribute('order.total_amount', total_amount)
|
171
|
+
RapiTapir::Observability::Tracing.set_attribute('order.item_count', item_count)
|
172
|
+
|
173
|
+
# Structured logging
|
174
|
+
RapiTapir::Observability::Logging.info(
|
175
|
+
'Processing order creation',
|
176
|
+
customer_id: customer_id,
|
177
|
+
total_amount: total_amount,
|
178
|
+
item_count: item_count,
|
179
|
+
tenant_id: tenant_id
|
180
|
+
)
|
181
|
+
|
182
|
+
begin
|
183
|
+
# Simulate order processing with nested spans
|
184
|
+
order_id = RapiTapir::Observability::Tracing.start_span('validate_order') do
|
185
|
+
# Validation logic
|
186
|
+
validate_order(order_data)
|
187
|
+
SecureRandom.uuid
|
188
|
+
end
|
189
|
+
|
190
|
+
RapiTapir::Observability::Tracing.start_span('process_payment') do |span|
|
191
|
+
span.set_attribute('payment.method', order_data[:payment_method][:type])
|
192
|
+
span.set_attribute('payment.amount', total_amount)
|
193
|
+
|
194
|
+
# Simulate payment processing
|
195
|
+
payment_result = process_payment(order_data[:payment_method], total_amount)
|
196
|
+
span.set_attribute('payment.transaction_id', payment_result[:transaction_id])
|
197
|
+
|
198
|
+
# Add payment event
|
199
|
+
RapiTapir::Observability::Tracing.add_event(
|
200
|
+
'payment.processed',
|
201
|
+
attributes: {
|
202
|
+
'payment.status' => payment_result[:status],
|
203
|
+
'payment.transaction_id' => payment_result[:transaction_id]
|
204
|
+
}
|
205
|
+
)
|
206
|
+
end
|
207
|
+
|
208
|
+
# Build response
|
209
|
+
response = {
|
210
|
+
id: order_id,
|
211
|
+
status: 'confirmed',
|
212
|
+
customer_id: customer_id,
|
213
|
+
total_amount: total_amount,
|
214
|
+
items: order_data[:items].map.with_index do |item, _index|
|
215
|
+
{
|
216
|
+
product_id: item[:product_id],
|
217
|
+
quantity: item[:quantity],
|
218
|
+
unit_price: item[:price],
|
219
|
+
total_price: item[:quantity] * item[:price]
|
220
|
+
}
|
221
|
+
end,
|
222
|
+
created_at: Time.now.utc.iso8601,
|
223
|
+
updated_at: Time.now.utc.iso8601
|
224
|
+
}
|
225
|
+
|
226
|
+
# Success event
|
227
|
+
RapiTapir::Observability::Tracing.add_event(
|
228
|
+
'order.created',
|
229
|
+
attributes: {
|
230
|
+
'order.id' => order_id,
|
231
|
+
'order.status' => 'confirmed'
|
232
|
+
}
|
233
|
+
)
|
234
|
+
|
235
|
+
# Success log
|
236
|
+
RapiTapir::Observability::Logging.info(
|
237
|
+
'Order created successfully',
|
238
|
+
customer_id: customer_id,
|
239
|
+
order_id: order_id,
|
240
|
+
total_amount: total_amount,
|
241
|
+
item_count: item_count,
|
242
|
+
tenant_id: tenant_id
|
243
|
+
)
|
244
|
+
|
245
|
+
response
|
246
|
+
rescue ValidationError => e
|
247
|
+
RapiTapir::Observability::Tracing.record_exception(e)
|
248
|
+
RapiTapir::Observability::Logging.log_error(
|
249
|
+
e,
|
250
|
+
customer_id: customer_id,
|
251
|
+
operation: 'order_validation',
|
252
|
+
tenant_id: tenant_id
|
253
|
+
)
|
254
|
+
raise
|
255
|
+
rescue PaymentError => e
|
256
|
+
RapiTapir::Observability::Tracing.record_exception(e)
|
257
|
+
RapiTapir::Observability::Logging.log_error(
|
258
|
+
e,
|
259
|
+
customer_id: customer_id,
|
260
|
+
operation: 'payment_processing',
|
261
|
+
tenant_id: tenant_id,
|
262
|
+
total_amount: total_amount
|
263
|
+
)
|
264
|
+
raise
|
265
|
+
rescue StandardError => e
|
266
|
+
RapiTapir::Observability::Tracing.record_exception(e)
|
267
|
+
RapiTapir::Observability::Logging.log_error(
|
268
|
+
e,
|
269
|
+
customer_id: customer_id,
|
270
|
+
operation: 'order_creation',
|
271
|
+
tenant_id: tenant_id
|
272
|
+
)
|
273
|
+
raise
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
# Helper methods for the example
|
278
|
+
def calculate_total_amount(items)
|
279
|
+
items.sum { |item| item[:quantity] * item[:price] }
|
280
|
+
end
|
281
|
+
|
282
|
+
def validate_order(order_data)
|
283
|
+
# Simulation of order validation
|
284
|
+
raise ValidationError, 'Invalid customer ID' if order_data[:customer_id].nil?
|
285
|
+
raise ValidationError, 'No items in order' if order_data[:items].empty?
|
286
|
+
|
287
|
+
order_data[:items].each do |item|
|
288
|
+
raise ValidationError, 'Invalid item quantity' if item[:quantity] <= 0
|
289
|
+
raise ValidationError, 'Invalid item price' if item[:price] <= 0
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
def process_payment(payment_method, amount)
|
294
|
+
# Simulation of payment processing
|
295
|
+
case payment_method[:type]
|
296
|
+
when 'credit_card'
|
297
|
+
# Simulate potential payment failure
|
298
|
+
raise PaymentError, 'Credit card declined' if amount > 10_000
|
299
|
+
|
300
|
+
{
|
301
|
+
status: 'success',
|
302
|
+
transaction_id: SecureRandom.hex(16)
|
303
|
+
}
|
304
|
+
when 'paypal'
|
305
|
+
{
|
306
|
+
status: 'success',
|
307
|
+
transaction_id: "PP_#{SecureRandom.hex(12)}"
|
308
|
+
}
|
309
|
+
when 'bank_transfer'
|
310
|
+
{
|
311
|
+
status: 'pending',
|
312
|
+
transaction_id: "BT_#{SecureRandom.hex(12)}"
|
313
|
+
}
|
314
|
+
else
|
315
|
+
raise PaymentError, 'Unsupported payment method'
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
# Custom exception classes
|
320
|
+
class ValidationError < StandardError; end
|
321
|
+
class PaymentError < StandardError; end
|
322
|
+
|
323
|
+
# Rack application with observability
|
324
|
+
class OrderApp
|
325
|
+
def call(env)
|
326
|
+
# Route to our endpoints
|
327
|
+
request = Rack::Request.new(env)
|
328
|
+
|
329
|
+
case request.path_info
|
330
|
+
when '/orders'
|
331
|
+
if request.request_method == 'POST'
|
332
|
+
create_order_endpoint.call(env)
|
333
|
+
else
|
334
|
+
[405, { 'Content-Type' => 'text/plain' }, ['Method Not Allowed']]
|
335
|
+
end
|
336
|
+
else
|
337
|
+
[404, { 'Content-Type' => 'text/plain' }, ['Not Found']]
|
338
|
+
end
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
# Build Rack app with observability middleware
|
343
|
+
Rack::Builder.new do
|
344
|
+
# Add observability middleware
|
345
|
+
use RapiTapir::Observability::RackMiddleware
|
346
|
+
|
347
|
+
# Your application
|
348
|
+
run OrderApp.new
|
349
|
+
end
|
350
|
+
|
351
|
+
puts 'Advanced observability example configured!'
|
352
|
+
puts 'Available endpoints:'
|
353
|
+
puts '- POST /orders (create order with full observability)'
|
354
|
+
puts '- GET /health (comprehensive health checks)'
|
355
|
+
puts '- GET /metrics (detailed Prometheus metrics)'
|
356
|
+
puts ''
|
357
|
+
puts 'Example request:'
|
358
|
+
puts 'curl -X POST http://localhost:9292/orders \\'
|
359
|
+
puts " -H 'Content-Type: application/json' \\"
|
360
|
+
puts " -H 'Authorization: Bearer your-jwt-token' \\"
|
361
|
+
puts " -H 'X-Tenant-ID: 123e4567-e89b-12d3-a456-426614174000' \\"
|
362
|
+
puts " -d '{"
|
363
|
+
puts ' "customer_id": "123e4567-e89b-12d3-a456-426614174000",'
|
364
|
+
puts ' "items": ['
|
365
|
+
puts ' {'
|
366
|
+
puts ' "product_id": "123e4567-e89b-12d3-a456-426614174001",'
|
367
|
+
puts ' "quantity": 2,'
|
368
|
+
puts ' "price": 29.99'
|
369
|
+
puts ' }'
|
370
|
+
puts ' ],'
|
371
|
+
puts ' "shipping_address": {'
|
372
|
+
puts ' "street": "123 Main St",'
|
373
|
+
puts ' "city": "Anytown",'
|
374
|
+
puts ' "state": "CA",'
|
375
|
+
puts ' "zip_code": "12345",'
|
376
|
+
puts ' "country": "US"'
|
377
|
+
puts ' },'
|
378
|
+
puts ' "payment_method": {'
|
379
|
+
puts ' "type": "credit_card",'
|
380
|
+
puts ' "details": {}'
|
381
|
+
puts ' }'
|
382
|
+
puts " }'"
|
383
|
+
puts ''
|
384
|
+
puts 'To run: rackup -p 9292'
|
@@ -0,0 +1,192 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rapitapir'
|
4
|
+
|
5
|
+
# Configure observability for the application
|
6
|
+
RapiTapir.configure do |config|
|
7
|
+
# Enable Prometheus metrics
|
8
|
+
config.metrics.enable_prometheus(
|
9
|
+
namespace: 'my_api',
|
10
|
+
labels: { service: 'user_service', version: '1.0.0' }
|
11
|
+
)
|
12
|
+
|
13
|
+
# Enable OpenTelemetry tracing
|
14
|
+
config.tracing.enable_opentelemetry(
|
15
|
+
service_name: 'my-api-service',
|
16
|
+
service_version: '1.0.0'
|
17
|
+
)
|
18
|
+
|
19
|
+
# Enable structured logging
|
20
|
+
config.logging.enable_structured(
|
21
|
+
level: :info,
|
22
|
+
fields: %i[timestamp level message request_id method path status duration user_id]
|
23
|
+
)
|
24
|
+
|
25
|
+
# Enable health checks
|
26
|
+
config.health_check.enable(endpoint: '/health')
|
27
|
+
|
28
|
+
# Add custom health checks
|
29
|
+
config.health_check.add_check(:database) do
|
30
|
+
# Simulate database health check
|
31
|
+
{ status: :healthy, message: 'Database connection OK' }
|
32
|
+
end
|
33
|
+
|
34
|
+
config.health_check.add_check(:redis) do
|
35
|
+
# Simulate Redis health check
|
36
|
+
{ status: :healthy, message: 'Redis connection OK' }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# Example endpoint with observability
|
41
|
+
RapiTapir.endpoint
|
42
|
+
.post
|
43
|
+
.in('/users')
|
44
|
+
.json_body({
|
45
|
+
name: :string,
|
46
|
+
email: :email,
|
47
|
+
age: { type: :integer, minimum: 18 }
|
48
|
+
})
|
49
|
+
.out_json({
|
50
|
+
id: :uuid,
|
51
|
+
name: :string,
|
52
|
+
email: :email,
|
53
|
+
created_at: :datetime
|
54
|
+
})
|
55
|
+
.with_metrics('user_creation')
|
56
|
+
.with_tracing
|
57
|
+
.with_logging(level: :info, fields: %i[user_email user_age])
|
58
|
+
.description('Create a new user')
|
59
|
+
.tag('users')
|
60
|
+
.handle do |request|
|
61
|
+
# Extract user data
|
62
|
+
user_data = request.body
|
63
|
+
|
64
|
+
# Add custom attributes to tracing span
|
65
|
+
RapiTapir::Observability::Tracing.set_attribute('user.email', user_data[:email])
|
66
|
+
RapiTapir::Observability::Tracing.set_attribute('user.age', user_data[:age])
|
67
|
+
|
68
|
+
# Log user creation attempt
|
69
|
+
RapiTapir::Observability::Logging.info(
|
70
|
+
'Creating user',
|
71
|
+
user_email: user_data[:email],
|
72
|
+
user_age: user_data[:age]
|
73
|
+
)
|
74
|
+
|
75
|
+
begin
|
76
|
+
# Simulate user creation with metrics
|
77
|
+
user_id = SecureRandom.uuid
|
78
|
+
created_at = Time.now.utc
|
79
|
+
|
80
|
+
# Add event to tracing span
|
81
|
+
RapiTapir::Observability::Tracing.add_event(
|
82
|
+
'user.created',
|
83
|
+
attributes: { 'user.id' => user_id }
|
84
|
+
)
|
85
|
+
|
86
|
+
# Return created user
|
87
|
+
{
|
88
|
+
id: user_id,
|
89
|
+
name: user_data[:name],
|
90
|
+
email: user_data[:email],
|
91
|
+
created_at: created_at.iso8601
|
92
|
+
}
|
93
|
+
rescue StandardError => e
|
94
|
+
# Record exception in tracing
|
95
|
+
RapiTapir::Observability::Tracing.record_exception(e)
|
96
|
+
|
97
|
+
# Log error with structured format
|
98
|
+
RapiTapir::Observability::Logging.log_error(
|
99
|
+
e,
|
100
|
+
user_email: user_data[:email],
|
101
|
+
operation: 'user_creation'
|
102
|
+
)
|
103
|
+
|
104
|
+
raise
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# Example endpoint with custom metrics
|
109
|
+
RapiTapir.endpoint
|
110
|
+
.get
|
111
|
+
.in('/users')
|
112
|
+
.query(:page, Types.integer(minimum: 1, default: 1))
|
113
|
+
.query(:limit, Types.integer(minimum: 1, maximum: 100, default: 20))
|
114
|
+
.out_json({
|
115
|
+
users: [{
|
116
|
+
id: :uuid,
|
117
|
+
name: :string,
|
118
|
+
email: :email
|
119
|
+
}],
|
120
|
+
pagination: {
|
121
|
+
current_page: :integer,
|
122
|
+
total_pages: :integer,
|
123
|
+
total_count: :integer
|
124
|
+
}
|
125
|
+
})
|
126
|
+
.with_metrics('user_list')
|
127
|
+
.with_tracing('GET /users')
|
128
|
+
.handle do |request|
|
129
|
+
page = request.query[:page] || 1
|
130
|
+
limit = request.query[:limit] || 20
|
131
|
+
|
132
|
+
# Custom span attributes
|
133
|
+
RapiTapir::Observability::Tracing.set_attribute('pagination.page', page)
|
134
|
+
RapiTapir::Observability::Tracing.set_attribute('pagination.limit', limit)
|
135
|
+
|
136
|
+
# Simulate fetching users
|
137
|
+
users = (1..limit).map do |i|
|
138
|
+
{
|
139
|
+
id: SecureRandom.uuid,
|
140
|
+
name: "User #{((page - 1) * limit) + i}",
|
141
|
+
email: "user#{((page - 1) * limit) + i}@example.com"
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
total_count = 1000 # Simulated total
|
146
|
+
total_pages = (total_count / limit.to_f).ceil
|
147
|
+
|
148
|
+
{
|
149
|
+
users: users,
|
150
|
+
pagination: {
|
151
|
+
current_page: page,
|
152
|
+
total_pages: total_pages,
|
153
|
+
total_count: total_count
|
154
|
+
}
|
155
|
+
}
|
156
|
+
end
|
157
|
+
|
158
|
+
# Error handling endpoint example
|
159
|
+
RapiTapir.endpoint
|
160
|
+
.get
|
161
|
+
.in('/error-test')
|
162
|
+
.out_json({ error: :string })
|
163
|
+
.with_metrics('error_test')
|
164
|
+
.with_tracing
|
165
|
+
.handle do |request|
|
166
|
+
# Simulate different types of errors for testing observability
|
167
|
+
error_type = request.query[:type] || 'generic'
|
168
|
+
|
169
|
+
case error_type
|
170
|
+
when 'timeout'
|
171
|
+
raise Timeout::Error, 'Operation timed out'
|
172
|
+
when 'validation'
|
173
|
+
raise ArgumentError, 'Invalid input data'
|
174
|
+
when 'not_found'
|
175
|
+
raise StandardError, 'Resource not found'
|
176
|
+
else
|
177
|
+
raise 'Generic error for testing'
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
puts 'Observability example configured!'
|
182
|
+
puts 'Available endpoints:'
|
183
|
+
puts '- POST /users (create user with full observability)'
|
184
|
+
puts '- GET /users (list users with pagination tracking)'
|
185
|
+
puts '- GET /error-test (test error tracking)'
|
186
|
+
puts '- GET /health (health check endpoint)'
|
187
|
+
puts '- GET /metrics (Prometheus metrics endpoint)'
|
188
|
+
puts ''
|
189
|
+
puts 'To run with a Rack server:'
|
190
|
+
puts "require 'rack'"
|
191
|
+
puts 'use RapiTapir::Observability::RackMiddleware'
|
192
|
+
puts 'run YourApp'
|
@@ -0,0 +1,121 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'net/http'
|
5
|
+
require 'json'
|
6
|
+
require 'uri'
|
7
|
+
|
8
|
+
puts "🍯 Complete Honeycomb.io Server Test"
|
9
|
+
puts "===================================="
|
10
|
+
|
11
|
+
# Change to project root
|
12
|
+
project_root = File.expand_path('../../', __dir__)
|
13
|
+
Dir.chdir(project_root)
|
14
|
+
|
15
|
+
puts "📂 Working directory: #{Dir.pwd}"
|
16
|
+
|
17
|
+
# Start server in background
|
18
|
+
puts "📡 Starting server..."
|
19
|
+
server_pid = spawn("bundle exec ruby examples/observability/honeycomb_working_example.rb",
|
20
|
+
out: "/tmp/honeycomb_server.log",
|
21
|
+
err: "/tmp/honeycomb_server.log")
|
22
|
+
|
23
|
+
# Give server time to start
|
24
|
+
puts "⏳ Waiting for server to start..."
|
25
|
+
server_ready = false
|
26
|
+
20.times do |i|
|
27
|
+
sleep 1
|
28
|
+
begin
|
29
|
+
response = Net::HTTP.get_response(URI('http://localhost:4567/health'))
|
30
|
+
if response.code == '200'
|
31
|
+
server_ready = true
|
32
|
+
puts "✅ Server is ready! (attempt #{i + 1})"
|
33
|
+
break
|
34
|
+
end
|
35
|
+
rescue Errno::ECONNREFUSED, Net::OpenTimeout
|
36
|
+
print "."
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
unless server_ready
|
41
|
+
puts "\n❌ Server failed to start within 20 seconds"
|
42
|
+
Process.kill('TERM', server_pid) rescue nil
|
43
|
+
Process.wait(server_pid) rescue nil
|
44
|
+
exit 1
|
45
|
+
end
|
46
|
+
|
47
|
+
puts "\n🧪 Running tests..."
|
48
|
+
|
49
|
+
# Test 1: Health check
|
50
|
+
puts "\n1️⃣ Testing health endpoint..."
|
51
|
+
response = Net::HTTP.get_response(URI('http://localhost:4567/health'))
|
52
|
+
puts " Status: #{response.code}"
|
53
|
+
puts " Body: #{response.body}"
|
54
|
+
|
55
|
+
# Test 2: List users (empty)
|
56
|
+
puts "\n2️⃣ Testing user list endpoint..."
|
57
|
+
response = Net::HTTP.get_response(URI('http://localhost:4567/users'))
|
58
|
+
puts " Status: #{response.code}"
|
59
|
+
puts " Body: #{response.body}"
|
60
|
+
|
61
|
+
# Test 3: Create a user
|
62
|
+
puts "\n3️⃣ Testing user creation..."
|
63
|
+
uri = URI('http://localhost:4567/users')
|
64
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
65
|
+
request = Net::HTTP::Post.new(uri)
|
66
|
+
request['Content-Type'] = 'application/json'
|
67
|
+
request.body = {
|
68
|
+
name: 'Alice Johnson',
|
69
|
+
email: 'alice@example.com',
|
70
|
+
age: 28,
|
71
|
+
department: 'engineering'
|
72
|
+
}.to_json
|
73
|
+
|
74
|
+
response = http.request(request)
|
75
|
+
puts " Status: #{response.code}"
|
76
|
+
puts " Body: #{response.body}"
|
77
|
+
|
78
|
+
if response.code == '201'
|
79
|
+
user_data = JSON.parse(response.body)
|
80
|
+
user_id = user_data['id']
|
81
|
+
|
82
|
+
# Test 4: Get the created user
|
83
|
+
puts "\n4️⃣ Testing get user by ID..."
|
84
|
+
response = Net::HTTP.get_response(URI("http://localhost:4567/users/#{user_id}"))
|
85
|
+
puts " Status: #{response.code}"
|
86
|
+
puts " Body: #{response.body}"
|
87
|
+
|
88
|
+
# Test 5: Update the user
|
89
|
+
puts "\n5️⃣ Testing user update..."
|
90
|
+
uri = URI("http://localhost:4567/users/#{user_id}")
|
91
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
92
|
+
request = Net::HTTP::Put.new(uri)
|
93
|
+
request['Content-Type'] = 'application/json'
|
94
|
+
request.body = {
|
95
|
+
name: 'Alice Johnson-Smith',
|
96
|
+
department: 'product'
|
97
|
+
}.to_json
|
98
|
+
|
99
|
+
response = http.request(request)
|
100
|
+
puts " Status: #{response.code}"
|
101
|
+
puts " Body: #{response.body}"
|
102
|
+
end
|
103
|
+
|
104
|
+
# Test 6: Department analytics
|
105
|
+
puts "\n6️⃣ Testing department analytics..."
|
106
|
+
response = Net::HTTP.get_response(URI('http://localhost:4567/analytics/department-stats'))
|
107
|
+
puts " Status: #{response.code}"
|
108
|
+
puts " Body: #{response.body}"
|
109
|
+
|
110
|
+
puts "\n✅ All tests completed!"
|
111
|
+
puts "\n📊 Check your Honeycomb.io dashboard for traces!"
|
112
|
+
puts " Dataset: rapitapir-demo"
|
113
|
+
puts " Service: rapitapir-demo"
|
114
|
+
|
115
|
+
# Cleanup
|
116
|
+
puts "\n🧹 Cleaning up..."
|
117
|
+
Process.kill('TERM', server_pid) rescue nil
|
118
|
+
Process.wait(server_pid) rescue nil
|
119
|
+
puts "✅ Server stopped"
|
120
|
+
|
121
|
+
puts "\n🎉 Test complete! Your Honeycomb integration is working!"
|