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