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,523 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Add local lib to load path
|
4
|
+
$LOAD_PATH.unshift(File.expand_path('../../lib', __dir__))
|
5
|
+
|
6
|
+
require 'rapitapir'
|
7
|
+
require 'sinatra/base'
|
8
|
+
require 'json'
|
9
|
+
require 'opentelemetry/sdk'
|
10
|
+
require 'opentelemetry/exporter/otlp'
|
11
|
+
require 'opentelemetry/instrumentation/all'
|
12
|
+
require 'opentelemetry/processor/baggage/baggage_span_processor'
|
13
|
+
|
14
|
+
# Initialize OpenTelemetry with Honeycomb.io configuration
|
15
|
+
OpenTelemetry::SDK.configure do |config|
|
16
|
+
# Add the BaggageSpanProcessor to include baggage in spans
|
17
|
+
config.add_span_processor(OpenTelemetry::Processor::Baggage::BaggageSpanProcessor.new)
|
18
|
+
|
19
|
+
# Add the BatchSpanProcessor with OTLP exporter for Honeycomb
|
20
|
+
config.add_span_processor(
|
21
|
+
OpenTelemetry::SDK::Trace::Export::BatchSpanProcessor.new(
|
22
|
+
OpenTelemetry::Exporter::OTLP::Exporter.new
|
23
|
+
)
|
24
|
+
)
|
25
|
+
|
26
|
+
# Use all available instrumentation (Sinatra, Rack, HTTP, etc.)
|
27
|
+
config.use_all
|
28
|
+
end
|
29
|
+
|
30
|
+
# RapiTapir configuration with observability
|
31
|
+
RapiTapir.configure do |config|
|
32
|
+
# Enable structured logging
|
33
|
+
config.logging.enable_structured(
|
34
|
+
level: :info,
|
35
|
+
fields: %i[
|
36
|
+
timestamp level message request_id
|
37
|
+
method path status duration
|
38
|
+
user_id service_name trace_id span_id
|
39
|
+
honeycomb_dataset
|
40
|
+
]
|
41
|
+
)
|
42
|
+
|
43
|
+
# Enable health checks for monitoring
|
44
|
+
config.health_check.enable(endpoint: '/health')
|
45
|
+
|
46
|
+
# Add database health check
|
47
|
+
config.health_check.add_check(:database) do
|
48
|
+
start_time = Time.now
|
49
|
+
# Simulate database connection check
|
50
|
+
sleep(0.01) # Simulate DB query time
|
51
|
+
duration = Time.now - start_time
|
52
|
+
|
53
|
+
{
|
54
|
+
status: :healthy,
|
55
|
+
message: 'Database connection successful',
|
56
|
+
response_time_ms: (duration * 1000).round(2)
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
# Add Redis health check
|
61
|
+
config.health_check.add_check(:redis) do
|
62
|
+
start_time = Time.now
|
63
|
+
# Simulate Redis ping
|
64
|
+
sleep(0.005)
|
65
|
+
duration = Time.now - start_time
|
66
|
+
|
67
|
+
{
|
68
|
+
status: :healthy,
|
69
|
+
message: 'Redis connection successful',
|
70
|
+
response_time_ms: (duration * 1000).round(2)
|
71
|
+
}
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Define API schemas with detailed field descriptions for better observability
|
76
|
+
USER_SCHEMA = {
|
77
|
+
id: { type: :uuid, description: 'Unique user identifier' },
|
78
|
+
name: { type: :string, min_length: 2, max_length: 100, description: 'Full name' },
|
79
|
+
email: { type: :email, description: 'User email address' },
|
80
|
+
age: { type: :integer, minimum: 18, maximum: 120, description: 'User age' },
|
81
|
+
department: { type: :string, enum: %w[engineering sales marketing support], description: 'Department' },
|
82
|
+
created_at: { type: :datetime, description: 'Account creation timestamp' }
|
83
|
+
}.freeze
|
84
|
+
|
85
|
+
# Custom RapiTapir extension for Honeycomb observability
|
86
|
+
module RapiTapir
|
87
|
+
module Extensions
|
88
|
+
module HoneycombObservability
|
89
|
+
def self.registered(app)
|
90
|
+
# Get the OpenTelemetry tracer for our application
|
91
|
+
tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-sinatra-app')
|
92
|
+
|
93
|
+
# Add before filter to start spans and add context
|
94
|
+
app.before do
|
95
|
+
# Start a new span for the request
|
96
|
+
@current_span = tracer.start_span(
|
97
|
+
"#{request.request_method} #{request.path_info}",
|
98
|
+
kind: :server
|
99
|
+
)
|
100
|
+
|
101
|
+
# Add standard HTTP attributes
|
102
|
+
@current_span.set_attribute('http.method', request.request_method)
|
103
|
+
@current_span.set_attribute('http.url', request.url)
|
104
|
+
@current_span.set_attribute('http.route', request.path_info)
|
105
|
+
@current_span.set_attribute('http.user_agent', request.user_agent) if request.user_agent
|
106
|
+
@current_span.set_attribute('service.name', 'rapitapir-demo')
|
107
|
+
@current_span.set_attribute('service.version', '1.0.0')
|
108
|
+
|
109
|
+
# Add custom business context via baggage
|
110
|
+
OpenTelemetry::Baggage.set_value('request.id', SecureRandom.uuid)
|
111
|
+
OpenTelemetry::Baggage.set_value('service.name', 'rapitapir-demo')
|
112
|
+
|
113
|
+
# Start timing the request
|
114
|
+
@request_start_time = Time.now
|
115
|
+
end
|
116
|
+
|
117
|
+
# Add after filter to complete spans
|
118
|
+
app.after do
|
119
|
+
next unless @current_span
|
120
|
+
|
121
|
+
# Calculate request duration
|
122
|
+
duration = Time.now - @request_start_time
|
123
|
+
@current_span.set_attribute('http.status_code', response.status)
|
124
|
+
@current_span.set_attribute('http.response_size', response.body.join.length)
|
125
|
+
@current_span.set_attribute('duration_ms', (duration * 1000).round(2))
|
126
|
+
|
127
|
+
# Set span status based on HTTP status
|
128
|
+
if response.status >= 400
|
129
|
+
@current_span.status = OpenTelemetry::Trace::Status.error("HTTP #{response.status}")
|
130
|
+
else
|
131
|
+
@current_span.status = OpenTelemetry::Trace::Status.ok
|
132
|
+
end
|
133
|
+
|
134
|
+
# Finish the span
|
135
|
+
@current_span.finish
|
136
|
+
end
|
137
|
+
|
138
|
+
# Helper method to add custom attributes to current span
|
139
|
+
app.helpers do
|
140
|
+
def add_span_attributes(**attributes)
|
141
|
+
return unless @current_span
|
142
|
+
|
143
|
+
attributes.each do |key, value|
|
144
|
+
@current_span.set_attribute(key.to_s, value)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def create_child_span(name, **attributes)
|
149
|
+
tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-sinatra-app')
|
150
|
+
span = tracer.start_span(name, with_parent: @current_span)
|
151
|
+
|
152
|
+
attributes.each do |key, value|
|
153
|
+
span.set_attribute(key.to_s, value)
|
154
|
+
end
|
155
|
+
|
156
|
+
yield(span) if block_given?
|
157
|
+
span
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Sinatra app with Honeycomb observability
|
166
|
+
class HoneycombDemoAPI < Sinatra::Base
|
167
|
+
register RapiTapir::Extensions::HoneycombObservability
|
168
|
+
|
169
|
+
# In-memory data store for demo
|
170
|
+
@@users = []
|
171
|
+
@@user_counter = 0
|
172
|
+
|
173
|
+
# Get tracer for business logic spans
|
174
|
+
def tracer
|
175
|
+
@tracer ||= OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
|
176
|
+
end
|
177
|
+
|
178
|
+
# GET /users - List all users with pagination and filtering
|
179
|
+
get '/users' do
|
180
|
+
tracer.in_span('users.list') do |span|
|
181
|
+
page = params[:page]&.to_i || 1
|
182
|
+
limit = params[:limit]&.to_i || 10
|
183
|
+
department = params[:department]
|
184
|
+
|
185
|
+
span.set_attribute('pagination.page', page)
|
186
|
+
span.set_attribute('pagination.limit', limit)
|
187
|
+
span.set_attribute('filter.department', department) if department
|
188
|
+
|
189
|
+
# Add custom business context
|
190
|
+
add_span_attributes(
|
191
|
+
'business.operation' => 'list_users',
|
192
|
+
'business.entity' => 'user'
|
193
|
+
)
|
194
|
+
|
195
|
+
# Simulate database query with child span
|
196
|
+
filtered_users = tracer.in_span('database.query.users') do |db_span|
|
197
|
+
db_span.set_attribute('db.operation', 'SELECT')
|
198
|
+
db_span.set_attribute('db.table', 'users')
|
199
|
+
|
200
|
+
# Simulate query time
|
201
|
+
sleep(0.02)
|
202
|
+
|
203
|
+
users = @@users.dup
|
204
|
+
users = users.select { |u| u[:department] == department } if department
|
205
|
+
users
|
206
|
+
end
|
207
|
+
|
208
|
+
# Pagination logic
|
209
|
+
start_index = (page - 1) * limit
|
210
|
+
paginated_users = filtered_users[start_index, limit] || []
|
211
|
+
|
212
|
+
span.set_attribute('result.count', paginated_users.length)
|
213
|
+
span.set_attribute('result.total_available', filtered_users.length)
|
214
|
+
|
215
|
+
content_type :json
|
216
|
+
{
|
217
|
+
users: paginated_users,
|
218
|
+
pagination: {
|
219
|
+
page: page,
|
220
|
+
limit: limit,
|
221
|
+
total: filtered_users.length,
|
222
|
+
has_more: start_index + limit < filtered_users.length
|
223
|
+
}
|
224
|
+
}.to_json
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# POST /users - Create a new user
|
229
|
+
post '/users' do
|
230
|
+
tracer.in_span('users.create') do |span|
|
231
|
+
begin
|
232
|
+
# Parse and validate input
|
233
|
+
request.body.rewind
|
234
|
+
user_data = JSON.parse(request.body.read)
|
235
|
+
|
236
|
+
span.set_attribute('business.operation', 'create_user')
|
237
|
+
span.set_attribute('business.entity', 'user')
|
238
|
+
span.set_attribute('input.department', user_data['department'])
|
239
|
+
|
240
|
+
# Validation with detailed tracing
|
241
|
+
validation_result = tracer.in_span('validation.user_input') do |validation_span|
|
242
|
+
errors = []
|
243
|
+
errors << 'Name is required' unless user_data['name']&.length&.between?(2, 100)
|
244
|
+
errors << 'Email is required' unless user_data['email']&.match?(/\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i)
|
245
|
+
errors << 'Age must be between 18 and 120' unless user_data['age']&.between?(18, 120)
|
246
|
+
errors << 'Invalid department' unless %w[engineering sales marketing support].include?(user_data['department'])
|
247
|
+
|
248
|
+
validation_span.set_attribute('validation.errors_count', errors.length)
|
249
|
+
validation_span.set_attribute('validation.passed', errors.empty?)
|
250
|
+
|
251
|
+
errors
|
252
|
+
end
|
253
|
+
|
254
|
+
unless validation_result.empty?
|
255
|
+
span.set_attribute('error.type', 'validation_error')
|
256
|
+
span.set_attribute('error.details', validation_result.join(', '))
|
257
|
+
span.status = OpenTelemetry::Trace::Status.error('Validation failed')
|
258
|
+
|
259
|
+
status 400
|
260
|
+
content_type :json
|
261
|
+
return { error: 'Validation failed', details: validation_result }.to_json
|
262
|
+
end
|
263
|
+
|
264
|
+
# Create user with database simulation
|
265
|
+
new_user = tracer.in_span('database.insert.user') do |db_span|
|
266
|
+
db_span.set_attribute('db.operation', 'INSERT')
|
267
|
+
db_span.set_attribute('db.table', 'users')
|
268
|
+
|
269
|
+
# Simulate database insert
|
270
|
+
sleep(0.015)
|
271
|
+
|
272
|
+
@@user_counter += 1
|
273
|
+
user = {
|
274
|
+
id: SecureRandom.uuid,
|
275
|
+
name: user_data['name'],
|
276
|
+
email: user_data['email'],
|
277
|
+
age: user_data['age'],
|
278
|
+
department: user_data['department'],
|
279
|
+
created_at: Time.now.iso8601
|
280
|
+
}
|
281
|
+
|
282
|
+
@@users << user
|
283
|
+
user
|
284
|
+
end
|
285
|
+
|
286
|
+
span.set_attribute('result.user_id', new_user[:id])
|
287
|
+
span.set_attribute('result.department', new_user[:department])
|
288
|
+
|
289
|
+
# Add success baggage for downstream spans
|
290
|
+
OpenTelemetry::Baggage.set_value('user.created', 'true')
|
291
|
+
OpenTelemetry::Baggage.set_value('user.id', new_user[:id])
|
292
|
+
|
293
|
+
status 201
|
294
|
+
content_type :json
|
295
|
+
new_user.to_json
|
296
|
+
|
297
|
+
rescue JSON::ParserError => e
|
298
|
+
span.set_attribute('error.type', 'json_parse_error')
|
299
|
+
span.set_attribute('error.message', e.message)
|
300
|
+
span.status = OpenTelemetry::Trace::Status.error('JSON parsing failed')
|
301
|
+
|
302
|
+
status 400
|
303
|
+
content_type :json
|
304
|
+
{ error: 'Invalid JSON format' }.to_json
|
305
|
+
rescue StandardError => e
|
306
|
+
span.set_attribute('error.type', 'internal_error')
|
307
|
+
span.set_attribute('error.message', e.message)
|
308
|
+
span.status = OpenTelemetry::Trace::Status.error('Internal server error')
|
309
|
+
|
310
|
+
status 500
|
311
|
+
content_type :json
|
312
|
+
{ error: 'Internal server error' }.to_json
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# GET /users/:id - Get a specific user
|
318
|
+
get '/users/:id' do |user_id|
|
319
|
+
tracer.in_span('users.get') do |span|
|
320
|
+
span.set_attribute('business.operation', 'get_user')
|
321
|
+
span.set_attribute('business.entity', 'user')
|
322
|
+
span.set_attribute('user.id', user_id)
|
323
|
+
|
324
|
+
# Find user with database simulation
|
325
|
+
user = tracer.in_span('database.query.user_by_id') do |db_span|
|
326
|
+
db_span.set_attribute('db.operation', 'SELECT')
|
327
|
+
db_span.set_attribute('db.table', 'users')
|
328
|
+
db_span.set_attribute('db.where', 'id = ?')
|
329
|
+
|
330
|
+
# Simulate database lookup
|
331
|
+
sleep(0.01)
|
332
|
+
|
333
|
+
@@users.find { |u| u[:id] == user_id }
|
334
|
+
end
|
335
|
+
|
336
|
+
if user
|
337
|
+
span.set_attribute('result.found', true)
|
338
|
+
span.set_attribute('result.department', user[:department])
|
339
|
+
|
340
|
+
content_type :json
|
341
|
+
user.to_json
|
342
|
+
else
|
343
|
+
span.set_attribute('result.found', false)
|
344
|
+
span.set_attribute('error.type', 'not_found')
|
345
|
+
|
346
|
+
status 404
|
347
|
+
content_type :json
|
348
|
+
{ error: 'User not found' }.to_json
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
# PUT /users/:id - Update a user
|
354
|
+
put '/users/:id' do |user_id|
|
355
|
+
tracer.in_span('users.update') do |span|
|
356
|
+
begin
|
357
|
+
request.body.rewind
|
358
|
+
update_data = JSON.parse(request.body.read)
|
359
|
+
|
360
|
+
span.set_attribute('business.operation', 'update_user')
|
361
|
+
span.set_attribute('business.entity', 'user')
|
362
|
+
span.set_attribute('user.id', user_id)
|
363
|
+
|
364
|
+
# Find and update user
|
365
|
+
user_index = @@users.find_index { |u| u[:id] == user_id }
|
366
|
+
|
367
|
+
unless user_index
|
368
|
+
span.set_attribute('error.type', 'not_found')
|
369
|
+
status 404
|
370
|
+
content_type :json
|
371
|
+
return { error: 'User not found' }.to_json
|
372
|
+
end
|
373
|
+
|
374
|
+
# Update with database simulation
|
375
|
+
updated_user = tracer.in_span('database.update.user') do |db_span|
|
376
|
+
db_span.set_attribute('db.operation', 'UPDATE')
|
377
|
+
db_span.set_attribute('db.table', 'users')
|
378
|
+
|
379
|
+
# Simulate database update
|
380
|
+
sleep(0.012)
|
381
|
+
|
382
|
+
user = @@users[user_index]
|
383
|
+
user[:name] = update_data['name'] if update_data['name']
|
384
|
+
user[:email] = update_data['email'] if update_data['email']
|
385
|
+
user[:age] = update_data['age'] if update_data['age']
|
386
|
+
user[:department] = update_data['department'] if update_data['department']
|
387
|
+
|
388
|
+
@@users[user_index] = user
|
389
|
+
user
|
390
|
+
end
|
391
|
+
|
392
|
+
span.set_attribute('result.updated', true)
|
393
|
+
span.set_attribute('result.department', updated_user[:department])
|
394
|
+
|
395
|
+
content_type :json
|
396
|
+
updated_user.to_json
|
397
|
+
|
398
|
+
rescue JSON::ParserError => e
|
399
|
+
span.set_attribute('error.type', 'json_parse_error')
|
400
|
+
span.set_attribute('error.message', e.message)
|
401
|
+
|
402
|
+
status 400
|
403
|
+
content_type :json
|
404
|
+
{ error: 'Invalid JSON format' }.to_json
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
# DELETE /users/:id - Delete a user
|
410
|
+
delete '/users/:id' do |user_id|
|
411
|
+
tracer.in_span('users.delete') do |span|
|
412
|
+
span.set_attribute('business.operation', 'delete_user')
|
413
|
+
span.set_attribute('business.entity', 'user')
|
414
|
+
span.set_attribute('user.id', user_id)
|
415
|
+
|
416
|
+
# Find and delete user
|
417
|
+
user_index = @@users.find_index { |u| u[:id] == user_id }
|
418
|
+
|
419
|
+
unless user_index
|
420
|
+
span.set_attribute('error.type', 'not_found')
|
421
|
+
status 404
|
422
|
+
content_type :json
|
423
|
+
return { error: 'User not found' }.to_json
|
424
|
+
end
|
425
|
+
|
426
|
+
# Delete with database simulation
|
427
|
+
deleted_user = tracer.in_span('database.delete.user') do |db_span|
|
428
|
+
db_span.set_attribute('db.operation', 'DELETE')
|
429
|
+
db_span.set_attribute('db.table', 'users')
|
430
|
+
|
431
|
+
# Simulate database delete
|
432
|
+
sleep(0.008)
|
433
|
+
|
434
|
+
@@users.delete_at(user_index)
|
435
|
+
end
|
436
|
+
|
437
|
+
span.set_attribute('result.deleted', true)
|
438
|
+
span.set_attribute('result.department', deleted_user[:department])
|
439
|
+
|
440
|
+
status 204
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# GET /analytics/department-stats - Business analytics endpoint
|
445
|
+
get '/analytics/department-stats' do
|
446
|
+
tracer.in_span('analytics.department_stats') do |span|
|
447
|
+
span.set_attribute('business.operation', 'department_analytics')
|
448
|
+
span.set_attribute('business.entity', 'analytics')
|
449
|
+
|
450
|
+
# Complex analytics with child spans
|
451
|
+
stats = tracer.in_span('analytics.compute.department_distribution') do |compute_span|
|
452
|
+
# Simulate complex computation
|
453
|
+
sleep(0.05)
|
454
|
+
|
455
|
+
departments = %w[engineering sales marketing support]
|
456
|
+
stats = departments.map do |dept|
|
457
|
+
count = @@users.count { |u| u[:department] == dept }
|
458
|
+
avg_age = @@users.select { |u| u[:department] == dept }
|
459
|
+
.map { |u| u[:age] }
|
460
|
+
.then { |ages| ages.empty? ? 0 : ages.sum.to_f / ages.length }
|
461
|
+
|
462
|
+
{
|
463
|
+
department: dept,
|
464
|
+
user_count: count,
|
465
|
+
average_age: avg_age.round(1)
|
466
|
+
}
|
467
|
+
end
|
468
|
+
|
469
|
+
compute_span.set_attribute('analytics.departments_analyzed', departments.length)
|
470
|
+
compute_span.set_attribute('analytics.total_users', @@users.length)
|
471
|
+
|
472
|
+
stats
|
473
|
+
end
|
474
|
+
|
475
|
+
span.set_attribute('result.departments_count', stats.length)
|
476
|
+
span.set_attribute('result.total_users', @@users.length)
|
477
|
+
|
478
|
+
content_type :json
|
479
|
+
{
|
480
|
+
timestamp: Time.now.iso8601,
|
481
|
+
total_users: @@users.length,
|
482
|
+
department_stats: stats
|
483
|
+
}.to_json
|
484
|
+
end
|
485
|
+
end
|
486
|
+
|
487
|
+
# Error handling with tracing
|
488
|
+
error do
|
489
|
+
tracer.in_span('error.handler') do |span|
|
490
|
+
error = env['sinatra.error']
|
491
|
+
span.set_attribute('error.type', error.class.name)
|
492
|
+
span.set_attribute('error.message', error.message)
|
493
|
+
span.status = OpenTelemetry::Trace::Status.error(error.message)
|
494
|
+
|
495
|
+
status 500
|
496
|
+
content_type :json
|
497
|
+
{ error: 'Internal server error', message: error.message }.to_json
|
498
|
+
end
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
# Configure the application to run
|
503
|
+
if __FILE__ == $PROGRAM_NAME
|
504
|
+
puts "🍯 Starting RapiTapir Demo API with Honeycomb.io Observability"
|
505
|
+
puts "📊 Traces will be sent to: #{ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] || 'https://api.honeycomb.io'}"
|
506
|
+
puts "🔧 Service Name: #{ENV['OTEL_SERVICE_NAME'] || 'rapitapir-demo'}"
|
507
|
+
puts ""
|
508
|
+
puts "🚀 Available endpoints:"
|
509
|
+
puts " GET /health - Health check"
|
510
|
+
puts " GET /users - List users (supports ?page=1&limit=10&department=engineering)"
|
511
|
+
puts " POST /users - Create user"
|
512
|
+
puts " GET /users/:id - Get user by ID"
|
513
|
+
puts " PUT /users/:id - Update user"
|
514
|
+
puts " DELETE /users/:id - Delete user"
|
515
|
+
puts " GET /analytics/department-stats - Department analytics"
|
516
|
+
puts ""
|
517
|
+
puts "📝 Example curl commands:"
|
518
|
+
puts " curl http://localhost:4567/users"
|
519
|
+
puts " curl -X POST http://localhost:4567/users -H 'Content-Type: application/json' -d '{\"name\":\"John Doe\",\"email\":\"john@example.com\",\"age\":30,\"department\":\"engineering\"}'"
|
520
|
+
puts ""
|
521
|
+
|
522
|
+
HoneycombDemoAPI.run!(host: '0.0.0.0', port: 4567)
|
523
|
+
end
|