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,203 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Observability
5
+ # Metrics collection and management system
6
+ # Provides counters, gauges, and histograms for monitoring
7
+ module Metrics
8
+ # Registry for metrics collection and storage
9
+ # Manages metric definitions and their current values
10
+ class Registry
11
+ def initialize
12
+ @metrics = {}
13
+ @provider = nil
14
+ end
15
+
16
+ def configure(provider: :prometheus, namespace: 'rapitapir', custom_labels: {})
17
+ @provider = provider
18
+ @namespace = namespace
19
+ @custom_labels = custom_labels
20
+
21
+ case provider
22
+ when :prometheus
23
+ configure_prometheus
24
+ end
25
+ end
26
+
27
+ def counter(name, help: '', labels: [])
28
+ metric_name = "#{@namespace}_#{name}"
29
+ return @metrics[metric_name] if @metrics[metric_name]
30
+
31
+ case @provider
32
+ when :prometheus
33
+ require 'prometheus/client'
34
+ @metrics[metric_name] = ::Prometheus::Client::Counter.new(
35
+ metric_name.to_sym,
36
+ docstring: help,
37
+ labels: labels + @custom_labels.keys
38
+ )
39
+ ::Prometheus::Client.registry.register(@metrics[metric_name])
40
+ end
41
+
42
+ @metrics[metric_name]
43
+ end
44
+
45
+ def histogram(name, help: '', labels: [], buckets: nil)
46
+ metric_name = "#{@namespace}_#{name}"
47
+ return @metrics[metric_name] if @metrics[metric_name]
48
+
49
+ case @provider
50
+ when :prometheus
51
+ require 'prometheus/client'
52
+ buckets ||= [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
53
+ @metrics[metric_name] = ::Prometheus::Client::Histogram.new(
54
+ metric_name.to_sym,
55
+ docstring: help,
56
+ labels: labels + @custom_labels.keys,
57
+ buckets: buckets
58
+ )
59
+ ::Prometheus::Client.registry.register(@metrics[metric_name])
60
+ end
61
+
62
+ @metrics[metric_name]
63
+ end
64
+
65
+ def gauge(name, help: '', labels: [])
66
+ metric_name = "#{@namespace}_#{name}"
67
+ return @metrics[metric_name] if @metrics[metric_name]
68
+
69
+ case @provider
70
+ when :prometheus
71
+ require 'prometheus/client'
72
+ @metrics[metric_name] = ::Prometheus::Client::Gauge.new(
73
+ metric_name.to_sym,
74
+ docstring: help,
75
+ labels: labels + @custom_labels.keys
76
+ )
77
+ ::Prometheus::Client.registry.register(@metrics[metric_name])
78
+ end
79
+
80
+ @metrics[metric_name]
81
+ end
82
+
83
+ def registry
84
+ case @provider
85
+ when :prometheus
86
+ require 'prometheus/client'
87
+ ::Prometheus::Client.registry
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ def configure_prometheus
94
+ require 'prometheus/client'
95
+ # Initialize default metrics
96
+ register_default_metrics
97
+ end
98
+
99
+ def register_default_metrics
100
+ # HTTP request metrics
101
+ counter(
102
+ :http_requests_total,
103
+ help: 'Total number of HTTP requests',
104
+ labels: %i[method endpoint status]
105
+ )
106
+
107
+ histogram(
108
+ :http_request_duration_seconds,
109
+ help: 'HTTP request duration in seconds',
110
+ labels: %i[method endpoint status]
111
+ )
112
+
113
+ # Error metrics
114
+ counter(
115
+ :http_errors_total,
116
+ help: 'Total number of HTTP errors',
117
+ labels: %i[method endpoint error_type]
118
+ )
119
+
120
+ # Active requests
121
+ gauge(
122
+ :http_active_requests,
123
+ help: 'Number of active HTTP requests',
124
+ labels: %i[method endpoint]
125
+ )
126
+ end
127
+ end
128
+
129
+ # Metrics collector for automatic metric gathering
130
+ # Automatically collects and updates metrics from various sources
131
+ class Collector
132
+ def initialize(registry)
133
+ @registry = registry
134
+ end
135
+
136
+ def record_request(method:, endpoint:, status:, duration:, error_type: nil)
137
+ labels = merge_custom_labels(method: method, endpoint: endpoint, status: status)
138
+
139
+ # Record request count
140
+ @registry.counter(:http_requests_total).increment(labels: labels)
141
+
142
+ # Record request duration
143
+ @registry.histogram(:http_request_duration_seconds).observe(duration, labels: labels)
144
+
145
+ # Record errors if present
146
+ return unless error_type
147
+
148
+ error_labels = merge_custom_labels(method: method, endpoint: endpoint, error_type: error_type)
149
+ @registry.counter(:http_errors_total).increment(labels: error_labels)
150
+ end
151
+
152
+ def increment_active_requests(method:, endpoint:)
153
+ labels = merge_custom_labels(method: method, endpoint: endpoint)
154
+ @registry.gauge(:http_active_requests).increment(labels: labels)
155
+ end
156
+
157
+ def decrement_active_requests(method:, endpoint:)
158
+ labels = merge_custom_labels(method: method, endpoint: endpoint)
159
+ @registry.gauge(:http_active_requests).decrement(labels: labels)
160
+ end
161
+
162
+ private
163
+
164
+ def merge_custom_labels(**labels)
165
+ custom_labels = @registry.instance_variable_get(:@custom_labels) || {}
166
+ labels.merge(custom_labels)
167
+ end
168
+ end
169
+
170
+ class << self
171
+ attr_reader :registry, :collector
172
+
173
+ def configure(provider: :prometheus, namespace: 'rapitapir', custom_labels: {})
174
+ @registry = Registry.new
175
+ @registry.configure(provider: provider, namespace: namespace, custom_labels: custom_labels)
176
+ @collector = Collector.new(@registry)
177
+ end
178
+
179
+ def enabled?
180
+ RapiTapir::Observability.config.metrics.enabled
181
+ end
182
+
183
+ def record_request(**args)
184
+ return unless enabled?
185
+
186
+ @collector&.record_request(**args)
187
+ end
188
+
189
+ def increment_active_requests(**args)
190
+ return unless enabled?
191
+
192
+ @collector&.increment_active_requests(**args)
193
+ end
194
+
195
+ def decrement_active_requests(**args)
196
+ return unless enabled?
197
+
198
+ @collector&.decrement_active_requests(**args)
199
+ end
200
+ end
201
+ end
202
+ end
203
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module RapiTapir
6
+ module Observability
7
+ # Observability middleware for request tracking
8
+ # Middleware that collects metrics, logs, and traces for each request
9
+ class Middleware
10
+ def initialize(app)
11
+ @app = app
12
+ end
13
+
14
+ def call(env)
15
+ request_data = build_request_data(env)
16
+
17
+ # Increment active requests metric
18
+ if Metrics.enabled?
19
+ Metrics.increment_active_requests(method: request_data[:method],
20
+ endpoint: request_data[:path])
21
+ end
22
+
23
+ # Start tracing span
24
+ span_attributes = build_span_attributes(request_data)
25
+
26
+ Tracing.start_span("HTTP #{request_data[:method]}", attributes: span_attributes, kind: :server) do |span|
27
+ process_request_with_observability(request_data, span)
28
+ rescue StandardError => e
29
+ handle_request_error(request_data, span, e)
30
+ ensure
31
+ # Decrement active requests metric
32
+ if Metrics.enabled?
33
+ Metrics.decrement_active_requests(method: request_data[:method],
34
+ endpoint: request_data[:path])
35
+ end
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def build_request_data(env)
42
+ request = Rack::Request.new(env)
43
+ request_id = extract_or_generate_request_id(env)
44
+
45
+ # Add request ID to environment for downstream use
46
+ env['HTTP_X_REQUEST_ID'] = request_id
47
+
48
+ {
49
+ request: request,
50
+ request_id: request_id,
51
+ method: request.request_method,
52
+ path: extract_path(request),
53
+ start_time: Time.now
54
+ }
55
+ end
56
+
57
+ def build_span_attributes(request_data)
58
+ request = request_data[:request]
59
+ {
60
+ 'http.method' => request_data[:method],
61
+ 'http.url' => request.url,
62
+ 'http.route' => request_data[:path],
63
+ 'http.user_agent' => request.user_agent,
64
+ 'request.id' => request_data[:request_id]
65
+ }
66
+ end
67
+
68
+ def process_request_with_observability(request_data, span)
69
+ # Process request
70
+ status, headers, response = @app.call(request_data[:request].env)
71
+ duration = Time.now - request_data[:start_time]
72
+
73
+ # Add response attributes to span
74
+ span.set_attribute('http.status_code', status)
75
+ span.set_attribute('http.response.size', calculate_response_size(response))
76
+
77
+ # Record metrics and logs
78
+ record_request_observability(request_data, status, duration)
79
+
80
+ # Add request ID to response headers
81
+ headers['X-Request-ID'] = request_data[:request_id]
82
+
83
+ [status, headers, response]
84
+ end
85
+
86
+ def record_request_observability(request_data, status, duration)
87
+ # Record metrics
88
+ record_metrics(
89
+ method: request_data[:method],
90
+ endpoint: request_data[:path],
91
+ status: status,
92
+ duration: duration
93
+ )
94
+
95
+ # Log request
96
+ log_request(
97
+ method: request_data[:method],
98
+ path: request_data[:path],
99
+ status: status,
100
+ duration: duration,
101
+ request_id: request_data[:request_id],
102
+ user_agent: request_data[:request].user_agent,
103
+ remote_ip: request_data[:request].ip
104
+ )
105
+ end
106
+
107
+ def handle_request_error(request_data, span, error)
108
+ duration = Time.now - request_data[:start_time]
109
+ error_type = error.class.name
110
+
111
+ # Record error in span
112
+ span.record_exception(error)
113
+ span.set_attribute('error', true)
114
+
115
+ # Record error metrics
116
+ record_metrics(
117
+ method: request_data[:method],
118
+ endpoint: request_data[:path],
119
+ status: 500,
120
+ duration: duration,
121
+ error_type: error_type
122
+ )
123
+
124
+ # Log error
125
+ Logging.log_error(
126
+ error,
127
+ request_id: request_data[:request_id],
128
+ method: request_data[:method],
129
+ path: request_data[:path]
130
+ )
131
+
132
+ raise
133
+ end
134
+
135
+ def extract_or_generate_request_id(env)
136
+ # Try to extract from headers (X-Request-ID, X-Correlation-ID, etc.)
137
+ request_id = env['HTTP_X_REQUEST_ID'] ||
138
+ env['HTTP_X_CORRELATION_ID'] ||
139
+ env['HTTP_X_TRACE_ID']
140
+
141
+ request_id || SecureRandom.hex(8)
142
+ end
143
+
144
+ def extract_path(request)
145
+ # Extract the route pattern if available, otherwise use path
146
+ request.env['REQUEST_URI'] || request.path_info || request.path
147
+ end
148
+
149
+ def calculate_response_size(response)
150
+ return 0 unless response.respond_to?(:each)
151
+
152
+ size = 0
153
+ response.each { |chunk| size += chunk.bytesize if chunk.respond_to?(:bytesize) }
154
+ size
155
+ end
156
+
157
+ def record_metrics(method:, endpoint:, status:, duration:, error_type: nil)
158
+ return unless Metrics.enabled?
159
+
160
+ Metrics.record_request(
161
+ method: method,
162
+ endpoint: endpoint,
163
+ status: status,
164
+ duration: duration,
165
+ error_type: error_type
166
+ )
167
+ end
168
+
169
+ def log_request(**options)
170
+ return unless Logging.enabled?
171
+
172
+ Logging.log_request(**options)
173
+ end
174
+ end
175
+
176
+ # HTTP endpoint for exposing metrics
177
+ # Provides an HTTP interface for metrics scraping (Prometheus format)
178
+ class MetricsEndpoint
179
+ def initialize(registry = nil)
180
+ @registry = registry || Metrics.registry&.registry
181
+ end
182
+
183
+ def call(env)
184
+ return not_found unless @registry && Metrics.enabled?
185
+
186
+ request = Rack::Request.new(env)
187
+
188
+ case request.path_info
189
+ when '/metrics'
190
+ handle_prometheus_metrics
191
+ else
192
+ not_found
193
+ end
194
+ end
195
+
196
+ private
197
+
198
+ def handle_prometheus_metrics
199
+ require 'prometheus/client'
200
+
201
+ output = ::Prometheus::Client::Formats::Text.marshal(@registry)
202
+
203
+ [200, {
204
+ 'Content-Type' => ::Prometheus::Client::Formats::Text::CONTENT_TYPE,
205
+ 'Cache-Control' => 'no-cache'
206
+ }, [output]]
207
+ rescue StandardError => e
208
+ [500, { 'Content-Type' => 'text/plain' }, ["Error generating metrics: #{e.message}"]]
209
+ end
210
+
211
+ def not_found
212
+ [404, { 'Content-Type' => 'text/plain' }, ['Not Found']]
213
+ end
214
+ end
215
+
216
+ # Rack middleware for adding observability to the stack
217
+ class RackMiddleware
218
+ def self.new(app, **_options)
219
+ # Build middleware stack
220
+ stack = app
221
+
222
+ # Add health check endpoint if enabled
223
+ if RapiTapir::Observability.config.health_check.enabled
224
+ stack = Rack::URLMap.new({
225
+ RapiTapir::Observability.config.health_check.endpoint => HealthCheck.endpoint,
226
+ '/' => stack
227
+ })
228
+ end
229
+
230
+ # Add metrics endpoint if enabled
231
+ if RapiTapir::Observability.config.metrics.enabled
232
+ stack = Rack::URLMap.new({
233
+ '/metrics' => MetricsEndpoint.new,
234
+ '/' => stack
235
+ })
236
+ end
237
+
238
+ # Add observability middleware
239
+ Middleware.new(stack)
240
+ end
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Observability
5
+ # Distributed tracing system for request flow tracking
6
+ # Provides span creation and management for tracing requests
7
+ module Tracing
8
+ # Tracer for creating and managing spans
9
+ # Creates spans for tracking request execution across services
10
+ class Tracer
11
+ def initialize(service_name:, service_version:)
12
+ @service_name = service_name
13
+ @service_version = service_version
14
+ @provider = nil
15
+ end
16
+
17
+ def configure(provider: :opentelemetry)
18
+ @provider = provider
19
+ case provider
20
+ when :opentelemetry
21
+ configure_opentelemetry
22
+ end
23
+ end
24
+
25
+ def start_span(name, attributes: {}, kind: :server)
26
+ return yield(NoOpSpan.new) unless enabled?
27
+
28
+ case @provider
29
+ when :opentelemetry
30
+ require 'opentelemetry/api'
31
+ tracer = OpenTelemetry.tracer_provider.tracer(@service_name, @service_version)
32
+ span = tracer.start_span(name, kind: kind, attributes: attributes)
33
+ OpenTelemetry::Context.with_current_span(span) do
34
+ yield(span)
35
+ ensure
36
+ span.finish
37
+ end
38
+ end
39
+ end
40
+
41
+ def current_span
42
+ return NoOpSpan.new unless enabled?
43
+
44
+ case @provider
45
+ when :opentelemetry
46
+ require 'opentelemetry/api'
47
+ OpenTelemetry::Context.current_span
48
+ end
49
+ end
50
+
51
+ def add_event(name, attributes: {})
52
+ return unless enabled?
53
+
54
+ current_span.add_event(name, attributes: attributes)
55
+ end
56
+
57
+ def set_attribute(key, value)
58
+ return unless enabled?
59
+
60
+ current_span.set_attribute(key, value)
61
+ end
62
+
63
+ def record_exception(exception)
64
+ return unless enabled?
65
+
66
+ current_span.record_exception(exception)
67
+ end
68
+
69
+ private
70
+
71
+ def enabled?
72
+ RapiTapir::Observability.config.tracing.enabled
73
+ end
74
+
75
+ def configure_opentelemetry
76
+ require 'opentelemetry/sdk'
77
+ require 'opentelemetry/instrumentation/all'
78
+
79
+ OpenTelemetry::SDK.configure do |c|
80
+ c.service_name = @service_name
81
+ c.service_version = @service_version
82
+ c.use_all # Use all available instrumentation
83
+ end
84
+ rescue LoadError
85
+ warn 'OpenTelemetry SDK not available. Install opentelemetry-sdk and ' \
86
+ 'opentelemetry-instrumentation-all gems.'
87
+ end
88
+ end
89
+
90
+ # No-operation span for disabled tracing
91
+ # Provides a null object pattern when tracing is disabled
92
+ class NoOpSpan
93
+ def add_event(name, attributes: {}); end
94
+ def set_attribute(key, value); end
95
+ def record_exception(exception); end
96
+ def finish; end
97
+ end
98
+
99
+ class << self
100
+ attr_reader :tracer
101
+
102
+ def configure(service_name:, service_version:, provider: :opentelemetry)
103
+ @tracer = Tracer.new(service_name: service_name, service_version: service_version)
104
+ @tracer.configure(provider: provider)
105
+ end
106
+
107
+ def enabled?
108
+ RapiTapir::Observability.config.tracing.enabled
109
+ end
110
+
111
+ def start_span(name, ...)
112
+ return yield(NoOpSpan.new) unless enabled?
113
+
114
+ @tracer&.start_span(name, ...)
115
+ end
116
+
117
+ def current_span
118
+ return NoOpSpan.new unless enabled?
119
+
120
+ @tracer&.current_span || NoOpSpan.new
121
+ end
122
+
123
+ def add_event(name, attributes: {})
124
+ return unless enabled?
125
+
126
+ @tracer&.add_event(name, attributes: attributes)
127
+ end
128
+
129
+ def set_attribute(key, value)
130
+ return unless enabled?
131
+
132
+ @tracer&.set_attribute(key, value)
133
+ end
134
+
135
+ def record_exception(exception)
136
+ return unless enabled?
137
+
138
+ @tracer&.record_exception(exception)
139
+ end
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'observability/configuration'
4
+ require_relative 'observability/metrics'
5
+ require_relative 'observability/tracing'
6
+ require_relative 'observability/logging'
7
+ require_relative 'observability/health_check'
8
+ require_relative 'observability/middleware'
9
+
10
+ module RapiTapir
11
+ # Observability features for monitoring and debugging APIs
12
+ # Provides metrics collection, logging, tracing, and health checking
13
+ module Observability
14
+ class << self
15
+ attr_accessor :configuration
16
+
17
+ def configure
18
+ self.configuration ||= Configuration.new
19
+ yield(configuration) if block_given?
20
+ configuration
21
+ end
22
+
23
+ def config
24
+ self.configuration ||= Configuration.new
25
+ end
26
+ end
27
+ end
28
+ end