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