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,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!"
|