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,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module Observability
|
5
|
+
# Configuration for observability features
|
6
|
+
# Manages settings for metrics, tracing, logging, and health checks
|
7
|
+
class Configuration
|
8
|
+
attr_accessor :metrics, :tracing, :logging, :health_check
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@metrics = MetricsConfig.new
|
12
|
+
@tracing = TracingConfig.new
|
13
|
+
@logging = LoggingConfig.new
|
14
|
+
@health_check = HealthCheckConfig.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Configuration for metrics collection
|
18
|
+
# Defines which metrics to collect and how to expose them
|
19
|
+
class MetricsConfig
|
20
|
+
attr_accessor :enabled, :provider, :namespace, :custom_labels
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@enabled = false
|
24
|
+
@provider = :prometheus
|
25
|
+
@namespace = 'rapitapir'
|
26
|
+
@custom_labels = {}
|
27
|
+
end
|
28
|
+
|
29
|
+
def enable_prometheus(namespace: 'rapitapir', labels: {})
|
30
|
+
@enabled = true
|
31
|
+
@provider = :prometheus
|
32
|
+
@namespace = namespace
|
33
|
+
@custom_labels = labels
|
34
|
+
end
|
35
|
+
|
36
|
+
def disable
|
37
|
+
@enabled = false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Configuration for distributed tracing
|
42
|
+
# Manages tracing setup and span collection settings
|
43
|
+
class TracingConfig
|
44
|
+
attr_accessor :enabled, :provider, :service_name, :service_version
|
45
|
+
|
46
|
+
def initialize
|
47
|
+
@enabled = false
|
48
|
+
@provider = :opentelemetry
|
49
|
+
@service_name = 'rapitapir-api'
|
50
|
+
@service_version = RapiTapir::VERSION
|
51
|
+
end
|
52
|
+
|
53
|
+
def enable_opentelemetry(service_name: 'rapitapir-api', service_version: nil)
|
54
|
+
@enabled = true
|
55
|
+
@provider = :opentelemetry
|
56
|
+
@service_name = service_name
|
57
|
+
@service_version = service_version || RapiTapir::VERSION
|
58
|
+
end
|
59
|
+
|
60
|
+
def disable
|
61
|
+
@enabled = false
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# Configuration for structured logging
|
66
|
+
# Controls log levels, formats, and output destinations
|
67
|
+
class LoggingConfig
|
68
|
+
attr_accessor :enabled, :structured, :level, :format, :fields
|
69
|
+
|
70
|
+
def initialize
|
71
|
+
@enabled = true
|
72
|
+
@structured = false
|
73
|
+
@level = :info
|
74
|
+
@format = :text
|
75
|
+
@fields = %i[timestamp level message request_id method path status duration]
|
76
|
+
end
|
77
|
+
|
78
|
+
def enable_structured(level: :info, fields: nil)
|
79
|
+
@enabled = true
|
80
|
+
@structured = true
|
81
|
+
@level = level
|
82
|
+
@fields = fields if fields
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# Configuration for health check endpoints
|
87
|
+
# Defines which health checks to run and how to expose them
|
88
|
+
class HealthCheckConfig
|
89
|
+
attr_accessor :enabled, :endpoint, :checks
|
90
|
+
|
91
|
+
def initialize
|
92
|
+
@enabled = false
|
93
|
+
@endpoint = '/health'
|
94
|
+
@checks = []
|
95
|
+
end
|
96
|
+
|
97
|
+
def enable(endpoint: '/health')
|
98
|
+
@enabled = true
|
99
|
+
@endpoint = endpoint
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_check(name, &block)
|
103
|
+
@checks << { name: name, check: block }
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module Observability
|
5
|
+
# Health check system for monitoring application status
|
6
|
+
# Provides health check registration and execution
|
7
|
+
module HealthCheck
|
8
|
+
# Individual health check definition
|
9
|
+
# Represents a single health check with execution logic
|
10
|
+
class Check
|
11
|
+
attr_reader :name, :description, :check_block
|
12
|
+
|
13
|
+
def initialize(name, description = nil, &block)
|
14
|
+
@name = name
|
15
|
+
@description = description || name.to_s.gsub('_', ' ').capitalize
|
16
|
+
@check_block = block
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
start_time = Time.now
|
21
|
+
|
22
|
+
begin
|
23
|
+
result = @check_block.call
|
24
|
+
duration = Time.now - start_time
|
25
|
+
process_check_result(result, duration)
|
26
|
+
rescue StandardError => e
|
27
|
+
duration = Time.now - start_time
|
28
|
+
create_result(:unhealthy, "#{e.class}: #{e.message}", duration)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def process_check_result(result, duration)
|
35
|
+
case result
|
36
|
+
when TrueClass, FalseClass
|
37
|
+
handle_boolean_result(result, duration)
|
38
|
+
when Hash
|
39
|
+
handle_hash_result(result, duration)
|
40
|
+
else
|
41
|
+
create_result(:healthy, result.to_s, duration)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def handle_boolean_result(result, duration)
|
46
|
+
status = result ? :healthy : :unhealthy
|
47
|
+
create_result(status, nil, duration)
|
48
|
+
end
|
49
|
+
|
50
|
+
def handle_hash_result(result, duration)
|
51
|
+
status = result.fetch(:status, :healthy)
|
52
|
+
message = result[:message]
|
53
|
+
create_result(status, message, duration)
|
54
|
+
end
|
55
|
+
|
56
|
+
def create_result(status, message, duration)
|
57
|
+
{
|
58
|
+
name: @name,
|
59
|
+
description: @description,
|
60
|
+
status: status,
|
61
|
+
message: message,
|
62
|
+
duration_ms: (duration * 1000).round(2),
|
63
|
+
timestamp: Time.now.utc.iso8601
|
64
|
+
}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Registry for managing health checks
|
69
|
+
# Centralized registry for registering and executing health checks
|
70
|
+
class Registry
|
71
|
+
def initialize
|
72
|
+
@checks = []
|
73
|
+
register_default_checks
|
74
|
+
end
|
75
|
+
|
76
|
+
def register(name, description = nil, &block)
|
77
|
+
@checks << Check.new(name, description, &block)
|
78
|
+
end
|
79
|
+
|
80
|
+
def run_all
|
81
|
+
results = @checks.map(&:call)
|
82
|
+
overall_status = results.all? { |r| r[:status] == :healthy } ? :healthy : :unhealthy
|
83
|
+
|
84
|
+
{
|
85
|
+
status: overall_status,
|
86
|
+
timestamp: Time.now.utc.iso8601,
|
87
|
+
service: 'rapitapir',
|
88
|
+
version: RapiTapir::VERSION,
|
89
|
+
checks: results
|
90
|
+
}
|
91
|
+
end
|
92
|
+
|
93
|
+
def run_check(name)
|
94
|
+
check = @checks.find { |c| c.name.to_s == name.to_s }
|
95
|
+
return { error: "Check '#{name}' not found" } unless check
|
96
|
+
|
97
|
+
check.call
|
98
|
+
end
|
99
|
+
|
100
|
+
def check_names
|
101
|
+
@checks.map(&:name)
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def register_default_checks
|
107
|
+
# Basic Ruby runtime check
|
108
|
+
register(:ruby_runtime, 'Ruby runtime health') do
|
109
|
+
{
|
110
|
+
status: :healthy,
|
111
|
+
message: "Ruby #{RUBY_VERSION} running on #{RUBY_PLATFORM}"
|
112
|
+
}
|
113
|
+
end
|
114
|
+
|
115
|
+
# Memory usage check
|
116
|
+
register(:memory_usage, 'Memory usage') do
|
117
|
+
if defined?(GC)
|
118
|
+
stats = GC.stat
|
119
|
+
{
|
120
|
+
status: :healthy,
|
121
|
+
message: "Heap size: #{stats[:heap_available_slots]}, Live objects: #{stats[:heap_live_slots]}"
|
122
|
+
}
|
123
|
+
else
|
124
|
+
{ status: :healthy, message: 'GC stats not available' }
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Thread count check
|
129
|
+
register(:thread_count, 'Active thread count') do
|
130
|
+
count = Thread.list.count
|
131
|
+
status = count > 100 ? :warning : :healthy
|
132
|
+
{
|
133
|
+
status: status,
|
134
|
+
message: "Active threads: #{count}"
|
135
|
+
}
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# HTTP endpoint for health check exposure
|
141
|
+
# Provides an HTTP interface for accessing health check results
|
142
|
+
class Endpoint
|
143
|
+
def initialize(registry, path = '/health')
|
144
|
+
@registry = registry
|
145
|
+
@path = path
|
146
|
+
end
|
147
|
+
|
148
|
+
def call(env)
|
149
|
+
request = Rack::Request.new(env)
|
150
|
+
|
151
|
+
case request.path_info
|
152
|
+
when @path
|
153
|
+
handle_overall_health
|
154
|
+
when "#{@path}/check"
|
155
|
+
handle_individual_check(request.params['name'])
|
156
|
+
when "#{@path}/checks"
|
157
|
+
handle_checks_list
|
158
|
+
else
|
159
|
+
[404, {}, ['Not Found']]
|
160
|
+
end
|
161
|
+
rescue StandardError => e
|
162
|
+
[500, { 'Content-Type' => 'application/json' }, [JSON.generate({
|
163
|
+
error: 'Internal server error',
|
164
|
+
message: e.message
|
165
|
+
})]]
|
166
|
+
end
|
167
|
+
|
168
|
+
private
|
169
|
+
|
170
|
+
def handle_overall_health
|
171
|
+
result = @registry.run_all
|
172
|
+
status_code = result[:status] == :healthy ? 200 : 503
|
173
|
+
|
174
|
+
[status_code, json_headers, [JSON.generate(result)]]
|
175
|
+
end
|
176
|
+
|
177
|
+
def handle_individual_check(name)
|
178
|
+
return [400, json_headers, [JSON.generate({ error: 'Missing check name parameter' })]] unless name
|
179
|
+
|
180
|
+
result = @registry.run_check(name)
|
181
|
+
status_code = result[:status] == :healthy ? 200 : 503
|
182
|
+
|
183
|
+
[status_code, json_headers, [JSON.generate(result)]]
|
184
|
+
end
|
185
|
+
|
186
|
+
def handle_checks_list
|
187
|
+
checks = @registry.check_names.map do |name|
|
188
|
+
{
|
189
|
+
name: name,
|
190
|
+
url: "#{@path}/check?name=#{name}"
|
191
|
+
}
|
192
|
+
end
|
193
|
+
|
194
|
+
[200, json_headers, [JSON.generate({
|
195
|
+
available_checks: checks,
|
196
|
+
total: checks.length
|
197
|
+
})]]
|
198
|
+
end
|
199
|
+
|
200
|
+
def json_headers
|
201
|
+
{ 'Content-Type' => 'application/json' }
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
class << self
|
206
|
+
attr_reader :registry
|
207
|
+
|
208
|
+
def configure(endpoint: '/health')
|
209
|
+
@registry = Registry.new
|
210
|
+
@endpoint_path = endpoint
|
211
|
+
end
|
212
|
+
|
213
|
+
def register(name, description = nil, &block)
|
214
|
+
@registry ||= Registry.new
|
215
|
+
@registry.register(name, description, &block)
|
216
|
+
end
|
217
|
+
|
218
|
+
def endpoint
|
219
|
+
@registry ||= Registry.new
|
220
|
+
Endpoint.new(@registry, @endpoint_path || '/health')
|
221
|
+
end
|
222
|
+
|
223
|
+
def enabled?
|
224
|
+
RapiTapir::Observability.config.health_check.enabled
|
225
|
+
end
|
226
|
+
|
227
|
+
def run_all
|
228
|
+
return { error: 'Health checks disabled' } unless enabled?
|
229
|
+
|
230
|
+
@registry ||= Registry.new
|
231
|
+
@registry.run_all
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,270 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'json'
|
5
|
+
require 'securerandom'
|
6
|
+
|
7
|
+
module RapiTapir
|
8
|
+
module Observability
|
9
|
+
# Structured logging system for RapiTapir
|
10
|
+
# Provides structured logging with multiple formatters and levels
|
11
|
+
module Logging
|
12
|
+
# Structured logger with contextual information
|
13
|
+
# Enhanced logger that adds structured data to log entries
|
14
|
+
class StructuredLogger
|
15
|
+
attr_reader :logger, :formatter
|
16
|
+
|
17
|
+
def initialize(output: $stdout, level: :info, format: :json)
|
18
|
+
@logger = ::Logger.new(output)
|
19
|
+
@logger.level = log_level(level)
|
20
|
+
@format = format
|
21
|
+
@formatter = create_formatter
|
22
|
+
@logger.formatter = @formatter
|
23
|
+
end
|
24
|
+
|
25
|
+
def debug(message = nil, **fields, &block)
|
26
|
+
log(:debug, message, **fields, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
def info(message = nil, **fields, &block)
|
30
|
+
log(:info, message, **fields, &block)
|
31
|
+
end
|
32
|
+
|
33
|
+
def warn(message = nil, **fields, &block)
|
34
|
+
log(:warn, message, **fields, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
def error(message = nil, **fields, &block)
|
38
|
+
log(:error, message, **fields, &block)
|
39
|
+
end
|
40
|
+
|
41
|
+
def fatal(message = nil, **fields, &block)
|
42
|
+
log(:fatal, message, **fields, &block)
|
43
|
+
end
|
44
|
+
|
45
|
+
def log_request(**options)
|
46
|
+
fields = build_request_log_fields(options)
|
47
|
+
status = options.fetch(:status)
|
48
|
+
method = options.fetch(:method)
|
49
|
+
path = options.fetch(:path)
|
50
|
+
|
51
|
+
level = determine_log_level_from_status(status)
|
52
|
+
message = build_request_log_message(method, path, status, fields[:duration_ms])
|
53
|
+
|
54
|
+
log(level, message, **fields)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def build_request_log_fields(options)
|
60
|
+
method = options.fetch(:method)
|
61
|
+
path = options.fetch(:path)
|
62
|
+
status = options.fetch(:status)
|
63
|
+
duration = options.fetch(:duration)
|
64
|
+
request_id = options[:request_id]
|
65
|
+
extra_fields = options.except(:method, :path, :status, :duration, :request_id)
|
66
|
+
|
67
|
+
request_data = {
|
68
|
+
method: method,
|
69
|
+
path: path,
|
70
|
+
status: status,
|
71
|
+
duration: duration,
|
72
|
+
request_id: request_id,
|
73
|
+
extra_fields: extra_fields
|
74
|
+
}
|
75
|
+
|
76
|
+
create_log_fields(request_data)
|
77
|
+
end
|
78
|
+
|
79
|
+
def create_log_fields(data)
|
80
|
+
{
|
81
|
+
event_type: 'http_request',
|
82
|
+
method: data[:method].to_s.upcase,
|
83
|
+
path: data[:path],
|
84
|
+
status: data[:status],
|
85
|
+
duration_ms: (data[:duration] * 1000).round(2),
|
86
|
+
request_id: data[:request_id] || generate_request_id
|
87
|
+
}.merge(data[:extra_fields])
|
88
|
+
end
|
89
|
+
|
90
|
+
def determine_log_level_from_status(status)
|
91
|
+
if status >= 500
|
92
|
+
:error
|
93
|
+
else
|
94
|
+
(status >= 400 ? :warn : :info)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def build_request_log_message(method, path, status, duration_ms)
|
99
|
+
"#{method.to_s.upcase} #{path} #{status} (#{duration_ms}ms)"
|
100
|
+
end
|
101
|
+
|
102
|
+
public
|
103
|
+
|
104
|
+
def log_error(exception, request_id: nil, **extra_fields)
|
105
|
+
fields = {
|
106
|
+
event_type: 'error',
|
107
|
+
error_class: exception.class.name,
|
108
|
+
error_message: exception.message,
|
109
|
+
error_backtrace: exception.backtrace&.first(10),
|
110
|
+
request_id: request_id
|
111
|
+
}.merge(extra_fields)
|
112
|
+
|
113
|
+
error("#{exception.class}: #{exception.message}", **fields)
|
114
|
+
end
|
115
|
+
|
116
|
+
private
|
117
|
+
|
118
|
+
def log(level, message = nil, **fields, &block)
|
119
|
+
return unless enabled?
|
120
|
+
|
121
|
+
message = block.call if block_given? && message.nil?
|
122
|
+
|
123
|
+
# Add common fields
|
124
|
+
fields = common_fields.merge(fields)
|
125
|
+
fields[:message] = message if message
|
126
|
+
|
127
|
+
@logger.public_send(level, fields)
|
128
|
+
end
|
129
|
+
|
130
|
+
def enabled?
|
131
|
+
RapiTapir::Observability.config.logging.enabled
|
132
|
+
end
|
133
|
+
|
134
|
+
def create_formatter
|
135
|
+
case @format
|
136
|
+
when :json
|
137
|
+
JsonFormatter.new
|
138
|
+
when :logfmt
|
139
|
+
LogfmtFormatter.new
|
140
|
+
else
|
141
|
+
TextFormatter.new
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def common_fields
|
146
|
+
{
|
147
|
+
timestamp: Time.now.utc.iso8601,
|
148
|
+
service: 'rapitapir',
|
149
|
+
version: RapiTapir::VERSION,
|
150
|
+
process_id: Process.pid
|
151
|
+
}
|
152
|
+
end
|
153
|
+
|
154
|
+
def log_level(level)
|
155
|
+
case level.to_sym
|
156
|
+
when :debug then ::Logger::DEBUG
|
157
|
+
when :warn then ::Logger::WARN
|
158
|
+
when :error then ::Logger::ERROR
|
159
|
+
when :fatal then ::Logger::FATAL
|
160
|
+
else ::Logger::INFO # Default for :info and unknown levels
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def generate_request_id
|
165
|
+
SecureRandom.hex(8)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
# JSON formatter for structured log output
|
170
|
+
# Formats log entries as JSON for machine-readable logs
|
171
|
+
class JsonFormatter
|
172
|
+
def call(severity, timestamp, _progname, msg)
|
173
|
+
case msg
|
174
|
+
when Hash
|
175
|
+
msg_with_metadata = msg.merge(
|
176
|
+
level: severity,
|
177
|
+
timestamp: timestamp.utc.iso8601
|
178
|
+
)
|
179
|
+
"#{JSON.generate(msg_with_metadata)}\n"
|
180
|
+
else
|
181
|
+
"#{JSON.generate(
|
182
|
+
level: severity,
|
183
|
+
timestamp: timestamp.utc.iso8601,
|
184
|
+
message: msg.to_s
|
185
|
+
)}\n"
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
# Logfmt formatter for key-value log output
|
191
|
+
# Formats log entries using the logfmt key=value format
|
192
|
+
class LogfmtFormatter
|
193
|
+
def call(severity, timestamp, _progname, msg)
|
194
|
+
case msg
|
195
|
+
when Hash
|
196
|
+
fields = msg.map { |k, v| "#{k}=#{format_value(v)}" }
|
197
|
+
"#{fields.join(' ')}\n"
|
198
|
+
else
|
199
|
+
"level=#{severity} timestamp=#{timestamp.utc.iso8601} message=#{format_value(msg)}\n"
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
private
|
204
|
+
|
205
|
+
def format_value(value)
|
206
|
+
case value
|
207
|
+
when String
|
208
|
+
value.include?(' ') ? "\"#{value}\"" : value
|
209
|
+
when Array
|
210
|
+
"[#{value.join(',')}]"
|
211
|
+
else
|
212
|
+
value.to_s
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Plain text formatter for human-readable logs
|
218
|
+
# Formats log entries as readable text for development
|
219
|
+
class TextFormatter
|
220
|
+
def call(severity, timestamp, _progname, msg)
|
221
|
+
case msg
|
222
|
+
when Hash
|
223
|
+
message = msg.delete(:message) || ''
|
224
|
+
extra = msg.map { |k, v| "#{k}=#{v}" }.join(' ')
|
225
|
+
"#{timestamp.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{message} #{extra}\n"
|
226
|
+
else
|
227
|
+
"#{timestamp.strftime('%Y-%m-%d %H:%M:%S')} [#{severity}] #{msg}\n"
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
class << self
|
233
|
+
attr_reader :logger
|
234
|
+
|
235
|
+
def configure(output: $stdout, level: :info, format: :json, structured: true)
|
236
|
+
if structured
|
237
|
+
@logger = StructuredLogger.new(output: output, level: level, format: format)
|
238
|
+
else
|
239
|
+
@logger = ::Logger.new(output)
|
240
|
+
@logger.level = log_level(level)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
def enabled?
|
245
|
+
RapiTapir::Observability.config.logging.enabled
|
246
|
+
end
|
247
|
+
|
248
|
+
%i[debug info warn error fatal].each do |level|
|
249
|
+
define_method(level) do |*args, **kwargs, &block|
|
250
|
+
return unless enabled?
|
251
|
+
|
252
|
+
@logger&.public_send(level, *args, **kwargs, &block)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def log_request(**args)
|
257
|
+
return unless enabled?
|
258
|
+
|
259
|
+
@logger&.log_request(**args)
|
260
|
+
end
|
261
|
+
|
262
|
+
def log_error(exception, **extra_fields)
|
263
|
+
return unless enabled?
|
264
|
+
|
265
|
+
@logger&.log_error(exception, **extra_fields)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|