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,488 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Add local lib to load path for development
|
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 'json'
|
15
|
+
require 'securerandom'
|
16
|
+
require 'opentelemetry/sdk'
|
17
|
+
require 'opentelemetry/exporter/otlp'
|
18
|
+
require 'opentelemetry/instrumentation/all'
|
19
|
+
require 'opentelemetry/processor/baggage/baggage_span_processor'
|
20
|
+
require 'rapitapir'
|
21
|
+
require 'rapitapir/sinatra_rapitapir'
|
22
|
+
|
23
|
+
# Initialize OpenTelemetry with Honeycomb.io configuration
|
24
|
+
OpenTelemetry::SDK.configure do |config|
|
25
|
+
config.use_all()
|
26
|
+
end
|
27
|
+
|
28
|
+
# Define schemas using RapiTapir Types
|
29
|
+
USER_SCHEMA = RapiTapir::Types.hash({
|
30
|
+
'id' => RapiTapir::Types.string(description: 'Unique user ID'),
|
31
|
+
'name' => RapiTapir::Types.string(min_length: 2, max_length: 100),
|
32
|
+
'email' => RapiTapir::Types.email,
|
33
|
+
'age' => RapiTapir::Types.integer(minimum: 18, maximum: 120),
|
34
|
+
'department' => RapiTapir::Types.string(enum: %w[engineering sales marketing support]),
|
35
|
+
'created_at' => RapiTapir::Types.string(description: 'ISO 8601 timestamp')
|
36
|
+
})
|
37
|
+
|
38
|
+
USER_CREATE_SCHEMA = RapiTapir::Types.hash({
|
39
|
+
'name' => RapiTapir::Types.string(min_length: 2, max_length: 100),
|
40
|
+
'email' => RapiTapir::Types.email,
|
41
|
+
'age' => RapiTapir::Types.integer(minimum: 18, maximum: 120),
|
42
|
+
'department' => RapiTapir::Types.string(enum: %w[engineering sales marketing support])
|
43
|
+
})
|
44
|
+
|
45
|
+
USER_UPDATE_SCHEMA = RapiTapir::Types.hash({
|
46
|
+
'name' => RapiTapir::Types.optional(RapiTapir::Types.string(min_length: 2, max_length: 100)),
|
47
|
+
'email' => RapiTapir::Types.optional(RapiTapir::Types.email),
|
48
|
+
'age' => RapiTapir::Types.optional(RapiTapir::Types.integer(minimum: 18, maximum: 120)),
|
49
|
+
'department' => RapiTapir::Types.optional(RapiTapir::Types.string(enum: %w[engineering sales marketing support]))
|
50
|
+
})
|
51
|
+
|
52
|
+
HEALTH_SCHEMA = RapiTapir::Types.hash({
|
53
|
+
'status' => RapiTapir::Types.string,
|
54
|
+
'timestamp' => RapiTapir::Types.string,
|
55
|
+
'service' => RapiTapir::Types.string,
|
56
|
+
'version' => RapiTapir::Types.string,
|
57
|
+
'checks' => RapiTapir::Types.hash({
|
58
|
+
'database' => RapiTapir::Types.hash({
|
59
|
+
'status' => RapiTapir::Types.string,
|
60
|
+
'response_time_ms' => RapiTapir::Types.float
|
61
|
+
}),
|
62
|
+
'redis' => RapiTapir::Types.hash({
|
63
|
+
'status' => RapiTapir::Types.string,
|
64
|
+
'response_time_ms' => RapiTapir::Types.float
|
65
|
+
})
|
66
|
+
})
|
67
|
+
})
|
68
|
+
|
69
|
+
USERS_LIST_SCHEMA = RapiTapir::Types.hash({
|
70
|
+
'users' => RapiTapir::Types.array(USER_SCHEMA),
|
71
|
+
'pagination' => RapiTapir::Types.hash({
|
72
|
+
'page' => RapiTapir::Types.integer,
|
73
|
+
'limit' => RapiTapir::Types.integer,
|
74
|
+
'total' => RapiTapir::Types.integer,
|
75
|
+
'has_more' => RapiTapir::Types.boolean
|
76
|
+
})
|
77
|
+
})
|
78
|
+
|
79
|
+
ANALYTICS_SCHEMA = RapiTapir::Types.hash({
|
80
|
+
'timestamp' => RapiTapir::Types.string,
|
81
|
+
'total_users' => RapiTapir::Types.integer,
|
82
|
+
'department_stats' => RapiTapir::Types.array(
|
83
|
+
RapiTapir::Types.hash({
|
84
|
+
'department' => RapiTapir::Types.string,
|
85
|
+
'user_count' => RapiTapir::Types.integer,
|
86
|
+
'average_age' => RapiTapir::Types.float
|
87
|
+
})
|
88
|
+
)
|
89
|
+
})
|
90
|
+
|
91
|
+
# Main API class using SinatraRapiTapir
|
92
|
+
class HoneycombDemoAPI < RapiTapir::SinatraRapiTapir
|
93
|
+
# In-memory data store for demo
|
94
|
+
@@users = []
|
95
|
+
@@user_counter = 0
|
96
|
+
|
97
|
+
# Configure RapiTapir
|
98
|
+
rapitapir do
|
99
|
+
info(
|
100
|
+
title: 'RapiTapir Demo API with Honeycomb.io',
|
101
|
+
description: 'Demonstrating observability integration with OpenTelemetry and Honeycomb',
|
102
|
+
version: '1.0.0',
|
103
|
+
contact: { email: 'demo@rapitapir.dev' }
|
104
|
+
)
|
105
|
+
|
106
|
+
development_defaults!
|
107
|
+
enable_docs(path: '/docs', openapi_path: '/openapi.json')
|
108
|
+
end
|
109
|
+
|
110
|
+
# Health Check Endpoint
|
111
|
+
endpoint(
|
112
|
+
GET('/health')
|
113
|
+
.summary('Health check')
|
114
|
+
.description('Returns the health status of the API with system checks')
|
115
|
+
.tags('Health')
|
116
|
+
.ok(HEALTH_SCHEMA)
|
117
|
+
.build
|
118
|
+
) do |inputs|
|
119
|
+
tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
|
120
|
+
tracer.in_span('health_check', attributes: { 'health_check.type' => 'basic' }) do |span|
|
121
|
+
health_data = {
|
122
|
+
status: 'healthy',
|
123
|
+
timestamp: Time.now.iso8601,
|
124
|
+
service: 'rapitapir-demo',
|
125
|
+
version: '1.0.0',
|
126
|
+
checks: {
|
127
|
+
database: { status: 'healthy', response_time_ms: 5.2 },
|
128
|
+
redis: { status: 'healthy', response_time_ms: 2.1 }
|
129
|
+
}
|
130
|
+
}
|
131
|
+
|
132
|
+
span.set_attribute('business.operation', 'health_check')
|
133
|
+
span.set_attribute('business.entity', 'system')
|
134
|
+
span.set_attribute('health_check.status', 'healthy')
|
135
|
+
span.set_attribute('health_check.checks_count', health_data[:checks].size)
|
136
|
+
|
137
|
+
health_data
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# List Users Endpoint
|
142
|
+
endpoint(
|
143
|
+
GET('/users')
|
144
|
+
.summary('List users')
|
145
|
+
.description('Get a paginated list of users with optional department filtering')
|
146
|
+
.tags('Users')
|
147
|
+
.query(:page, RapiTapir::Types.optional(RapiTapir::Types.integer(minimum: 1)), description: 'Page number')
|
148
|
+
.query(:limit, RapiTapir::Types.optional(RapiTapir::Types.integer(minimum: 1, maximum: 100)), description: 'Items per page')
|
149
|
+
.query(:department, RapiTapir::Types.optional(RapiTapir::Types.string(enum: %w[engineering sales marketing support])), description: 'Filter by department')
|
150
|
+
.ok(USERS_LIST_SCHEMA)
|
151
|
+
.build
|
152
|
+
) do |inputs|
|
153
|
+
tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
|
154
|
+
tracer.in_span('users.list') do |span|
|
155
|
+
page = inputs[:page] || 1
|
156
|
+
limit = inputs[:limit] || 10
|
157
|
+
department = inputs[:department]
|
158
|
+
|
159
|
+
span.set_attribute('business.operation', 'list_users')
|
160
|
+
span.set_attribute('business.entity', 'user')
|
161
|
+
span.set_attribute('pagination.page', page)
|
162
|
+
span.set_attribute('pagination.limit', limit)
|
163
|
+
span.set_attribute('filter.department', department) if department
|
164
|
+
|
165
|
+
# Simulate database query
|
166
|
+
tracer.in_span('database.select.users') do |db_span|
|
167
|
+
db_span.set_attribute('db.operation', 'SELECT')
|
168
|
+
db_span.set_attribute('db.table', 'users')
|
169
|
+
sleep(0.02)
|
170
|
+
end
|
171
|
+
|
172
|
+
filtered_users = @@users.dup
|
173
|
+
filtered_users = filtered_users.select { |u| u[:department] == department } if department
|
174
|
+
|
175
|
+
# Pagination
|
176
|
+
start_index = (page - 1) * limit
|
177
|
+
paginated_users = filtered_users[start_index, limit] || []
|
178
|
+
|
179
|
+
span.set_attribute('result.count', paginated_users.length)
|
180
|
+
span.set_attribute('result.total_available', filtered_users.length)
|
181
|
+
|
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
|
+
}
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Create User Endpoint
|
195
|
+
endpoint(
|
196
|
+
POST('/users')
|
197
|
+
.summary('Create user')
|
198
|
+
.description('Create a new user with validation')
|
199
|
+
.tags('Users')
|
200
|
+
.json_body(USER_CREATE_SCHEMA)
|
201
|
+
.created(USER_SCHEMA)
|
202
|
+
.bad_request(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string, 'details' => RapiTapir::Types.array(RapiTapir::Types.string) }))
|
203
|
+
.build
|
204
|
+
) do |inputs|
|
205
|
+
tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
|
206
|
+
tracer.in_span('users.create') do |span|
|
207
|
+
user_data = inputs[:body]
|
208
|
+
|
209
|
+
span.set_attribute('business.operation', 'create_user')
|
210
|
+
span.set_attribute('business.entity', 'user')
|
211
|
+
span.set_attribute('input.department', user_data['department'])
|
212
|
+
|
213
|
+
# Validate input
|
214
|
+
validation_errors = tracer.in_span('validation.user_input') do |validation_span|
|
215
|
+
errors = []
|
216
|
+
existing_email = @@users.find { |u| u[:email] == user_data['email'] }
|
217
|
+
errors << 'Email already exists' if existing_email
|
218
|
+
|
219
|
+
validation_span.set_attribute('validation.errors_count', errors.length)
|
220
|
+
validation_span.set_attribute('validation.passed', errors.empty?)
|
221
|
+
|
222
|
+
errors
|
223
|
+
end
|
224
|
+
|
225
|
+
unless validation_errors.empty?
|
226
|
+
span.set_attribute('error.type', 'validation_error')
|
227
|
+
span.set_attribute('error.details', validation_errors.join(', '))
|
228
|
+
span.status = OpenTelemetry::Trace::Status.error('Validation failed')
|
229
|
+
|
230
|
+
content_type :json
|
231
|
+
status 400
|
232
|
+
return { error: 'Validation failed', details: validation_errors }
|
233
|
+
end
|
234
|
+
|
235
|
+
# Create user
|
236
|
+
new_user = tracer.in_span('database.insert.user') do |db_span|
|
237
|
+
db_span.set_attribute('db.operation', 'INSERT')
|
238
|
+
db_span.set_attribute('db.table', 'users')
|
239
|
+
sleep(0.015)
|
240
|
+
|
241
|
+
@@user_counter += 1
|
242
|
+
user = {
|
243
|
+
id: SecureRandom.uuid,
|
244
|
+
name: user_data['name'],
|
245
|
+
email: user_data['email'],
|
246
|
+
age: user_data['age'],
|
247
|
+
department: user_data['department'],
|
248
|
+
created_at: Time.now.iso8601
|
249
|
+
}
|
250
|
+
|
251
|
+
@@users << user
|
252
|
+
user
|
253
|
+
end
|
254
|
+
|
255
|
+
span.set_attribute('result.user_id', new_user[:id])
|
256
|
+
span.set_attribute('result.department', new_user[:department])
|
257
|
+
|
258
|
+
# Add success baggage
|
259
|
+
OpenTelemetry::Baggage.set_value('user.created', 'true')
|
260
|
+
OpenTelemetry::Baggage.set_value('user.id', new_user[:id])
|
261
|
+
|
262
|
+
new_user
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
266
|
+
# Get User by ID Endpoint
|
267
|
+
endpoint(
|
268
|
+
GET('/users/{id}')
|
269
|
+
.summary('Get user')
|
270
|
+
.description('Get a specific user by ID')
|
271
|
+
.tags('Users')
|
272
|
+
.path_param(:id, RapiTapir::Types.string, description: 'User ID')
|
273
|
+
.ok(USER_SCHEMA)
|
274
|
+
.not_found(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string }))
|
275
|
+
.build
|
276
|
+
) do |inputs|
|
277
|
+
tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
|
278
|
+
tracer.in_span('users.get') do |span|
|
279
|
+
user_id = inputs[:id]
|
280
|
+
|
281
|
+
span.set_attribute('business.operation', 'get_user')
|
282
|
+
span.set_attribute('business.entity', 'user')
|
283
|
+
span.set_attribute('user.id', user_id)
|
284
|
+
|
285
|
+
# Find user
|
286
|
+
user = tracer.in_span('database.select.user', attributes: { 'db.operation' => 'SELECT', 'db.table' => 'users' }) do
|
287
|
+
sleep(0.01)
|
288
|
+
@@users.find { |u| u[:id] == user_id }
|
289
|
+
end
|
290
|
+
|
291
|
+
if user
|
292
|
+
span.set_attribute('result.found', true)
|
293
|
+
span.set_attribute('result.department', user[:department])
|
294
|
+
user
|
295
|
+
else
|
296
|
+
span.set_attribute('result.found', false)
|
297
|
+
span.set_attribute('error.type', 'not_found')
|
298
|
+
return [404, { 'Content-Type' => 'application/json' }, [{ error: 'User not found' }.to_json]]
|
299
|
+
end
|
300
|
+
end
|
301
|
+
end
|
302
|
+
|
303
|
+
# Update User Endpoint
|
304
|
+
endpoint(
|
305
|
+
PUT('/users/{id}')
|
306
|
+
.summary('Update user')
|
307
|
+
.description('Update an existing user')
|
308
|
+
.tags('Users')
|
309
|
+
.path_param(:id, RapiTapir::Types.string, description: 'User ID')
|
310
|
+
.json_body(USER_UPDATE_SCHEMA)
|
311
|
+
.ok(USER_SCHEMA)
|
312
|
+
.not_found(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string }))
|
313
|
+
.build
|
314
|
+
) do |inputs|
|
315
|
+
tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
|
316
|
+
tracer.in_span('users.update') do |span|
|
317
|
+
user_id = inputs[:id]
|
318
|
+
update_data = inputs[:body]
|
319
|
+
|
320
|
+
span.set_attribute('business.operation', 'update_user')
|
321
|
+
span.set_attribute('business.entity', 'user')
|
322
|
+
span.set_attribute('user.id', user_id)
|
323
|
+
|
324
|
+
# Find user index
|
325
|
+
user_index = @@users.find_index { |u| u[:id] == user_id }
|
326
|
+
|
327
|
+
unless user_index
|
328
|
+
span.set_attribute('error.type', 'not_found')
|
329
|
+
return [404, { 'Content-Type' => 'application/json' }, [{ error: 'User not found' }.to_json]]
|
330
|
+
end
|
331
|
+
|
332
|
+
# Update user
|
333
|
+
updated_user = tracer.in_span('database.update.user') do |db_span|
|
334
|
+
db_span.set_attribute('db.operation', 'UPDATE')
|
335
|
+
db_span.set_attribute('db.table', 'users')
|
336
|
+
sleep(0.012)
|
337
|
+
|
338
|
+
user = @@users[user_index]
|
339
|
+
user[:name] = update_data['name'] if update_data['name']
|
340
|
+
user[:email] = update_data['email'] if update_data['email']
|
341
|
+
user[:age] = update_data['age'] if update_data['age']
|
342
|
+
user[:department] = update_data['department'] if update_data['department']
|
343
|
+
|
344
|
+
@@users[user_index] = user
|
345
|
+
user
|
346
|
+
end
|
347
|
+
|
348
|
+
span.set_attribute('result.updated', true)
|
349
|
+
span.set_attribute('result.department', updated_user[:department])
|
350
|
+
|
351
|
+
updated_user
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
# Delete User Endpoint
|
356
|
+
endpoint(
|
357
|
+
DELETE('/users/{id}')
|
358
|
+
.summary('Delete user')
|
359
|
+
.description('Delete a user by ID')
|
360
|
+
.tags('Users')
|
361
|
+
.path_param(:id, RapiTapir::Types.string, description: 'User ID')
|
362
|
+
.no_content
|
363
|
+
.not_found(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string }))
|
364
|
+
.build
|
365
|
+
) do |inputs|
|
366
|
+
tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
|
367
|
+
tracer.in_span('users.delete') do |span|
|
368
|
+
user_id = inputs[:id]
|
369
|
+
|
370
|
+
span.set_attribute('business.operation', 'delete_user')
|
371
|
+
span.set_attribute('business.entity', 'user')
|
372
|
+
span.set_attribute('user.id', user_id)
|
373
|
+
|
374
|
+
# Find user index
|
375
|
+
user_index = @@users.find_index { |u| u[:id] == user_id }
|
376
|
+
|
377
|
+
unless user_index
|
378
|
+
span.set_attribute('error.type', 'not_found')
|
379
|
+
return [404, { 'Content-Type' => 'application/json' }, [{ error: 'User not found' }.to_json]]
|
380
|
+
end
|
381
|
+
|
382
|
+
# Delete user
|
383
|
+
deleted_user = tracer.in_span('database.delete.user') do |db_span|
|
384
|
+
db_span.set_attribute('db.operation', 'DELETE')
|
385
|
+
db_span.set_attribute('db.table', 'users')
|
386
|
+
sleep(0.008)
|
387
|
+
|
388
|
+
@@users.delete_at(user_index)
|
389
|
+
end
|
390
|
+
|
391
|
+
span.set_attribute('result.deleted', true)
|
392
|
+
span.set_attribute('result.department', deleted_user[:department])
|
393
|
+
|
394
|
+
nil # 204 No Content
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
# Analytics Endpoint
|
399
|
+
endpoint(
|
400
|
+
GET('/analytics/department-stats')
|
401
|
+
.summary('Department analytics')
|
402
|
+
.description('Get analytics data grouped by department')
|
403
|
+
.tags('Analytics')
|
404
|
+
.ok(ANALYTICS_SCHEMA)
|
405
|
+
.build
|
406
|
+
) do |inputs|
|
407
|
+
tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
|
408
|
+
tracer.in_span('analytics.department_stats') do |span|
|
409
|
+
span.set_attribute('business.operation', 'department_analytics')
|
410
|
+
span.set_attribute('business.entity', 'analytics')
|
411
|
+
|
412
|
+
# Complex analytics computation
|
413
|
+
stats = tracer.in_span('analytics.compute.department_distribution') do |compute_span|
|
414
|
+
sleep(0.05)
|
415
|
+
|
416
|
+
departments = %w[engineering sales marketing support]
|
417
|
+
department_stats = departments.map do |dept|
|
418
|
+
count = @@users.count { |u| u[:department] == dept }
|
419
|
+
avg_age = if count > 0
|
420
|
+
ages = @@users.select { |u| u[:department] == dept }.map { |u| u[:age] }
|
421
|
+
ages.sum.to_f / ages.length
|
422
|
+
else
|
423
|
+
0
|
424
|
+
end
|
425
|
+
|
426
|
+
{
|
427
|
+
department: dept,
|
428
|
+
user_count: count,
|
429
|
+
average_age: avg_age.round(1)
|
430
|
+
}
|
431
|
+
end
|
432
|
+
|
433
|
+
compute_span.set_attribute('analytics.departments_analyzed', departments.length)
|
434
|
+
compute_span.set_attribute('analytics.total_users', @@users.length)
|
435
|
+
|
436
|
+
department_stats
|
437
|
+
end
|
438
|
+
|
439
|
+
span.set_attribute('result.departments_count', stats.length)
|
440
|
+
span.set_attribute('result.total_users', @@users.length)
|
441
|
+
|
442
|
+
{
|
443
|
+
timestamp: Time.now.iso8601,
|
444
|
+
total_users: @@users.length,
|
445
|
+
department_stats: stats
|
446
|
+
}
|
447
|
+
end
|
448
|
+
end
|
449
|
+
|
450
|
+
# Error handling with tracing
|
451
|
+
error do
|
452
|
+
tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
|
453
|
+
tracer.in_span('error.handler') do |span|
|
454
|
+
error = env['sinatra.error']
|
455
|
+
span.set_attribute('error.type', error.class.name)
|
456
|
+
span.set_attribute('error.message', error.message)
|
457
|
+
span.status = OpenTelemetry::Trace::Status.error(error.message)
|
458
|
+
|
459
|
+
content_type :json
|
460
|
+
{ error: 'Internal server error', message: error.message }.to_json
|
461
|
+
end
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
# Configure and run the application
|
466
|
+
if __FILE__ == $PROGRAM_NAME
|
467
|
+
puts "🍯 Starting RapiTapir Demo API with Honeycomb.io Observability"
|
468
|
+
puts "📊 Traces will be sent to: #{ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] || 'https://api.honeycomb.io'}"
|
469
|
+
puts "🔧 Service Name: #{ENV['OTEL_SERVICE_NAME'] || 'rapitapir-demo'}"
|
470
|
+
puts ""
|
471
|
+
puts "🚀 Available endpoints:"
|
472
|
+
puts " GET /health - Health check"
|
473
|
+
puts " GET /users - List users (supports ?page=1&limit=10&department=engineering)"
|
474
|
+
puts " POST /users - Create user"
|
475
|
+
puts " GET /users/{id} - Get user by ID"
|
476
|
+
puts " PUT /users/{id} - Update user"
|
477
|
+
puts " DELETE /users/{id} - Delete user"
|
478
|
+
puts " GET /analytics/department-stats - Department analytics"
|
479
|
+
puts " GET /docs - Swagger UI documentation"
|
480
|
+
puts " GET /openapi.json - OpenAPI specification"
|
481
|
+
puts ""
|
482
|
+
puts "📝 Example curl commands:"
|
483
|
+
puts " curl http://localhost:4567/users"
|
484
|
+
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\"}'"
|
485
|
+
puts ""
|
486
|
+
|
487
|
+
HoneycombDemoAPI.run!(host: '0.0.0.0', port: 4567)
|
488
|
+
end
|