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.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +57 -0
  4. data/CHANGELOG.md +94 -0
  5. data/CLEANUP_SUMMARY.md +155 -0
  6. data/CONTRIBUTING.md +280 -0
  7. data/LICENSE +21 -0
  8. data/README.md +485 -0
  9. data/debug_hash.rb +20 -0
  10. data/docs/EXTENSION_COMPARISON.md +388 -0
  11. data/docs/SINATRA_EXTENSION.md +467 -0
  12. data/docs/archive/PHASE_1_2_COMPLETE.md +77 -0
  13. data/docs/archive/PHASE_1_3_COMPLETE.md +152 -0
  14. data/docs/archive/PHASE_2_1_OBSERVABILITY_COMPLETED.md +203 -0
  15. data/docs/archive/PHASE_2_SUMMARY.md +209 -0
  16. data/docs/archive/REFACTORING_SUMMARY.md +184 -0
  17. data/docs/archive/phase_1_3_plan.md +136 -0
  18. data/docs/archive/sinatra_extension_summary.md +188 -0
  19. data/docs/archive/sinatra_working_solution.md +113 -0
  20. data/docs/archive/typescript-client-generator-summary.md +259 -0
  21. data/docs/auto-derivation.md +146 -0
  22. data/docs/blueprint.md +1091 -0
  23. data/docs/endpoint-definition.md +211 -0
  24. data/docs/github_pages_fix.md +52 -0
  25. data/docs/github_pages_setup.md +49 -0
  26. data/docs/implementation-status.md +357 -0
  27. data/docs/observability.md +647 -0
  28. data/docs/phase3-plan.md +108 -0
  29. data/docs/sinatra_rapitapir.md +87 -0
  30. data/docs/type_shortcuts.md +146 -0
  31. data/examples/README_ENTERPRISE.md +202 -0
  32. data/examples/authentication_example.rb +192 -0
  33. data/examples/auto_derivation_ruby_friendly.rb +163 -0
  34. data/examples/cli/user_api_endpoints.rb +56 -0
  35. data/examples/client/typescript_client_example.rb +102 -0
  36. data/examples/client/user-api-client.ts +193 -0
  37. data/examples/demo_api.rb +41 -0
  38. data/examples/docs/documentation_example.rb +112 -0
  39. data/examples/docs/user-api-docs.html +789 -0
  40. data/examples/docs/user-api-docs.md +403 -0
  41. data/examples/enhanced_auto_derivation_test.rb +83 -0
  42. data/examples/enterprise_extension_demo.rb +417 -0
  43. data/examples/enterprise_rapitapir_api.rb +662 -0
  44. data/examples/getting_started_extension.rb +218 -0
  45. data/examples/hello_world.rb +74 -0
  46. data/examples/oauth2/.env.example +19 -0
  47. data/examples/oauth2/README.md +205 -0
  48. data/examples/oauth2/generic_oauth2_api.rb +226 -0
  49. data/examples/oauth2/get_token.rb +72 -0
  50. data/examples/oauth2/songs_api_with_auth0.rb +320 -0
  51. data/examples/oauth2/test_api.sh +16 -0
  52. data/examples/oauth2/test_songs_api.sh +110 -0
  53. data/examples/observability/.env.example +35 -0
  54. data/examples/observability/README.md +230 -0
  55. data/examples/observability/README_HONEYCOMB.md +332 -0
  56. data/examples/observability/advanced_setup.rb +384 -0
  57. data/examples/observability/basic_setup.rb +192 -0
  58. data/examples/observability/complete_test.rb +121 -0
  59. data/examples/observability/honeycomb_example.rb +523 -0
  60. data/examples/observability/honeycomb_rapitapir_clean.rb +488 -0
  61. data/examples/observability/honeycomb_rapitapir_example.rb +523 -0
  62. data/examples/observability/honeycomb_working_example.rb +489 -0
  63. data/examples/observability/quick_test.rb +78 -0
  64. data/examples/observability/simple_test.rb +14 -0
  65. data/examples/observability/test_honeycomb_demo.rb +354 -0
  66. data/examples/observability/test_live_honeycomb.rb +111 -0
  67. data/examples/observability/test_validation.rb +78 -0
  68. data/examples/observability/test_working_validation.rb +66 -0
  69. data/examples/openapi/user_api_schema.rb +132 -0
  70. data/examples/production_ready_example.rb +105 -0
  71. data/examples/rails/users_controller.rb +146 -0
  72. data/examples/readme/basic_sinatra_example.rb +128 -0
  73. data/examples/server/user_api.rb +179 -0
  74. data/examples/simple_auto_derivation_demo.rb +44 -0
  75. data/examples/simple_demo_api.rb +18 -0
  76. data/examples/sinatra/user_app.rb +127 -0
  77. data/examples/t_shortcut_demo.rb +59 -0
  78. data/examples/user_api.rb +190 -0
  79. data/examples/working_getting_started.rb +184 -0
  80. data/examples/working_simple_example.rb +195 -0
  81. data/lib/rapitapir/auth/configuration.rb +129 -0
  82. data/lib/rapitapir/auth/context.rb +122 -0
  83. data/lib/rapitapir/auth/errors.rb +104 -0
  84. data/lib/rapitapir/auth/middleware.rb +324 -0
  85. data/lib/rapitapir/auth/oauth2.rb +350 -0
  86. data/lib/rapitapir/auth/schemes.rb +420 -0
  87. data/lib/rapitapir/auth.rb +113 -0
  88. data/lib/rapitapir/cli/command.rb +535 -0
  89. data/lib/rapitapir/cli/server.rb +243 -0
  90. data/lib/rapitapir/cli/validator.rb +373 -0
  91. data/lib/rapitapir/client/generator_base.rb +272 -0
  92. data/lib/rapitapir/client/typescript_generator.rb +350 -0
  93. data/lib/rapitapir/core/endpoint.rb +158 -0
  94. data/lib/rapitapir/core/enhanced_endpoint.rb +235 -0
  95. data/lib/rapitapir/core/input.rb +182 -0
  96. data/lib/rapitapir/core/output.rb +164 -0
  97. data/lib/rapitapir/core/request.rb +19 -0
  98. data/lib/rapitapir/core/response.rb +17 -0
  99. data/lib/rapitapir/docs/html_generator.rb +780 -0
  100. data/lib/rapitapir/docs/markdown_generator.rb +464 -0
  101. data/lib/rapitapir/dsl/endpoint_dsl.rb +116 -0
  102. data/lib/rapitapir/dsl/enhanced_endpoint_dsl.rb +62 -0
  103. data/lib/rapitapir/dsl/enhanced_input.rb +73 -0
  104. data/lib/rapitapir/dsl/enhanced_output.rb +63 -0
  105. data/lib/rapitapir/dsl/enhanced_structures.rb +393 -0
  106. data/lib/rapitapir/dsl/fluent_dsl.rb +72 -0
  107. data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +316 -0
  108. data/lib/rapitapir/dsl/http_verbs.rb +77 -0
  109. data/lib/rapitapir/dsl/input_methods.rb +47 -0
  110. data/lib/rapitapir/dsl/observability_methods.rb +81 -0
  111. data/lib/rapitapir/dsl/output_methods.rb +43 -0
  112. data/lib/rapitapir/dsl/type_resolution.rb +43 -0
  113. data/lib/rapitapir/observability/configuration.rb +108 -0
  114. data/lib/rapitapir/observability/health_check.rb +236 -0
  115. data/lib/rapitapir/observability/logging.rb +270 -0
  116. data/lib/rapitapir/observability/metrics.rb +203 -0
  117. data/lib/rapitapir/observability/middleware.rb +243 -0
  118. data/lib/rapitapir/observability/tracing.rb +143 -0
  119. data/lib/rapitapir/observability.rb +28 -0
  120. data/lib/rapitapir/openapi/schema_generator.rb +403 -0
  121. data/lib/rapitapir/schema.rb +136 -0
  122. data/lib/rapitapir/server/enhanced_rack_adapter.rb +379 -0
  123. data/lib/rapitapir/server/middleware.rb +120 -0
  124. data/lib/rapitapir/server/path_matcher.rb +45 -0
  125. data/lib/rapitapir/server/rack_adapter.rb +215 -0
  126. data/lib/rapitapir/server/rails_adapter.rb +17 -0
  127. data/lib/rapitapir/server/rails_adapter_class.rb +53 -0
  128. data/lib/rapitapir/server/rails_controller.rb +72 -0
  129. data/lib/rapitapir/server/rails_input_processor.rb +73 -0
  130. data/lib/rapitapir/server/rails_response_handler.rb +29 -0
  131. data/lib/rapitapir/server/sinatra_adapter.rb +200 -0
  132. data/lib/rapitapir/server/sinatra_integration.rb +93 -0
  133. data/lib/rapitapir/sinatra/configuration.rb +91 -0
  134. data/lib/rapitapir/sinatra/extension.rb +214 -0
  135. data/lib/rapitapir/sinatra/oauth2_helpers.rb +236 -0
  136. data/lib/rapitapir/sinatra/resource_builder.rb +152 -0
  137. data/lib/rapitapir/sinatra/swagger_ui_generator.rb +166 -0
  138. data/lib/rapitapir/sinatra_rapitapir.rb +40 -0
  139. data/lib/rapitapir/types/array.rb +163 -0
  140. data/lib/rapitapir/types/auto_derivation.rb +265 -0
  141. data/lib/rapitapir/types/base.rb +146 -0
  142. data/lib/rapitapir/types/boolean.rb +46 -0
  143. data/lib/rapitapir/types/date.rb +92 -0
  144. data/lib/rapitapir/types/datetime.rb +98 -0
  145. data/lib/rapitapir/types/email.rb +32 -0
  146. data/lib/rapitapir/types/float.rb +134 -0
  147. data/lib/rapitapir/types/hash.rb +161 -0
  148. data/lib/rapitapir/types/integer.rb +143 -0
  149. data/lib/rapitapir/types/object.rb +156 -0
  150. data/lib/rapitapir/types/optional.rb +65 -0
  151. data/lib/rapitapir/types/string.rb +185 -0
  152. data/lib/rapitapir/types/uuid.rb +32 -0
  153. data/lib/rapitapir/types.rb +155 -0
  154. data/lib/rapitapir/version.rb +5 -0
  155. data/lib/rapitapir.rb +173 -0
  156. data/rapitapir.gemspec +66 -0
  157. 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