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