a2a-ruby 1.0.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 (128) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +137 -0
  4. data/.simplecov +46 -0
  5. data/.yardopts +10 -0
  6. data/CHANGELOG.md +33 -0
  7. data/CODE_OF_CONDUCT.md +128 -0
  8. data/CONTRIBUTING.md +165 -0
  9. data/Gemfile +43 -0
  10. data/Guardfile +34 -0
  11. data/LICENSE.txt +21 -0
  12. data/PUBLISHING_CHECKLIST.md +214 -0
  13. data/README.md +171 -0
  14. data/Rakefile +165 -0
  15. data/docs/agent_execution.md +309 -0
  16. data/docs/api_reference.md +792 -0
  17. data/docs/configuration.md +780 -0
  18. data/docs/events.md +475 -0
  19. data/docs/getting_started.md +668 -0
  20. data/docs/integration.md +262 -0
  21. data/docs/server_apps.md +621 -0
  22. data/docs/troubleshooting.md +765 -0
  23. data/lib/a2a/client/api_methods.rb +263 -0
  24. data/lib/a2a/client/auth/api_key.rb +161 -0
  25. data/lib/a2a/client/auth/interceptor.rb +288 -0
  26. data/lib/a2a/client/auth/jwt.rb +189 -0
  27. data/lib/a2a/client/auth/oauth2.rb +146 -0
  28. data/lib/a2a/client/auth.rb +137 -0
  29. data/lib/a2a/client/base.rb +316 -0
  30. data/lib/a2a/client/config.rb +210 -0
  31. data/lib/a2a/client/connection_pool.rb +233 -0
  32. data/lib/a2a/client/http_client.rb +524 -0
  33. data/lib/a2a/client/json_rpc_handler.rb +136 -0
  34. data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
  35. data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
  36. data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
  37. data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
  38. data/lib/a2a/client/middleware.rb +116 -0
  39. data/lib/a2a/client/performance_tracker.rb +60 -0
  40. data/lib/a2a/configuration/defaults.rb +34 -0
  41. data/lib/a2a/configuration/environment_loader.rb +76 -0
  42. data/lib/a2a/configuration/file_loader.rb +115 -0
  43. data/lib/a2a/configuration/inheritance.rb +101 -0
  44. data/lib/a2a/configuration/validator.rb +180 -0
  45. data/lib/a2a/configuration.rb +201 -0
  46. data/lib/a2a/errors.rb +291 -0
  47. data/lib/a2a/modules.rb +50 -0
  48. data/lib/a2a/monitoring/alerting.rb +490 -0
  49. data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
  50. data/lib/a2a/monitoring/health_endpoints.rb +204 -0
  51. data/lib/a2a/monitoring/metrics_collector.rb +438 -0
  52. data/lib/a2a/monitoring.rb +463 -0
  53. data/lib/a2a/plugin.rb +358 -0
  54. data/lib/a2a/plugin_manager.rb +159 -0
  55. data/lib/a2a/plugins/example_auth.rb +81 -0
  56. data/lib/a2a/plugins/example_middleware.rb +118 -0
  57. data/lib/a2a/plugins/example_transport.rb +76 -0
  58. data/lib/a2a/protocol/agent_card.rb +8 -0
  59. data/lib/a2a/protocol/agent_card_server.rb +584 -0
  60. data/lib/a2a/protocol/capability.rb +496 -0
  61. data/lib/a2a/protocol/json_rpc.rb +254 -0
  62. data/lib/a2a/protocol/message.rb +8 -0
  63. data/lib/a2a/protocol/task.rb +8 -0
  64. data/lib/a2a/rails/a2a_controller.rb +258 -0
  65. data/lib/a2a/rails/controller_helpers.rb +499 -0
  66. data/lib/a2a/rails/engine.rb +167 -0
  67. data/lib/a2a/rails/generators/agent_generator.rb +311 -0
  68. data/lib/a2a/rails/generators/install_generator.rb +209 -0
  69. data/lib/a2a/rails/generators/migration_generator.rb +232 -0
  70. data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
  71. data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
  72. data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
  73. data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
  74. data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
  75. data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
  76. data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
  77. data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
  78. data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
  79. data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
  80. data/lib/a2a/rails/tasks/a2a.rake +228 -0
  81. data/lib/a2a/server/a2a_methods.rb +520 -0
  82. data/lib/a2a/server/agent.rb +537 -0
  83. data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
  84. data/lib/a2a/server/agent_execution/request_context.rb +219 -0
  85. data/lib/a2a/server/apps/rack_app.rb +311 -0
  86. data/lib/a2a/server/apps/sinatra_app.rb +261 -0
  87. data/lib/a2a/server/default_request_handler.rb +350 -0
  88. data/lib/a2a/server/events/event_consumer.rb +116 -0
  89. data/lib/a2a/server/events/event_queue.rb +226 -0
  90. data/lib/a2a/server/example_agent.rb +248 -0
  91. data/lib/a2a/server/handler.rb +281 -0
  92. data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
  93. data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
  94. data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
  95. data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
  96. data/lib/a2a/server/middleware.rb +213 -0
  97. data/lib/a2a/server/push_notification_manager.rb +327 -0
  98. data/lib/a2a/server/request_handler.rb +136 -0
  99. data/lib/a2a/server/storage/base.rb +141 -0
  100. data/lib/a2a/server/storage/database.rb +266 -0
  101. data/lib/a2a/server/storage/memory.rb +274 -0
  102. data/lib/a2a/server/storage/redis.rb +320 -0
  103. data/lib/a2a/server/storage.rb +38 -0
  104. data/lib/a2a/server/task_manager.rb +534 -0
  105. data/lib/a2a/transport/grpc.rb +481 -0
  106. data/lib/a2a/transport/http.rb +415 -0
  107. data/lib/a2a/transport/sse.rb +499 -0
  108. data/lib/a2a/types/agent_card.rb +540 -0
  109. data/lib/a2a/types/artifact.rb +99 -0
  110. data/lib/a2a/types/base_model.rb +223 -0
  111. data/lib/a2a/types/events.rb +117 -0
  112. data/lib/a2a/types/message.rb +106 -0
  113. data/lib/a2a/types/part.rb +288 -0
  114. data/lib/a2a/types/push_notification.rb +139 -0
  115. data/lib/a2a/types/security.rb +167 -0
  116. data/lib/a2a/types/task.rb +154 -0
  117. data/lib/a2a/types.rb +88 -0
  118. data/lib/a2a/utils/helpers.rb +245 -0
  119. data/lib/a2a/utils/message_buffer.rb +278 -0
  120. data/lib/a2a/utils/performance.rb +247 -0
  121. data/lib/a2a/utils/rails_detection.rb +97 -0
  122. data/lib/a2a/utils/structured_logger.rb +306 -0
  123. data/lib/a2a/utils/time_helpers.rb +167 -0
  124. data/lib/a2a/utils/validation.rb +8 -0
  125. data/lib/a2a/version.rb +6 -0
  126. data/lib/a2a-rails.rb +58 -0
  127. data/lib/a2a.rb +198 -0
  128. metadata +437 -0
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Circuit breaker interceptor for fault tolerance
5
+ #
6
+ # Implements the circuit breaker pattern to prevent cascading failures
7
+ # by temporarily stopping requests to a failing service.
8
+ #
9
+ module A2A
10
+ module Client
11
+ module Middleware
12
+ class CircuitBreakerInterceptor
13
+ # Circuit breaker states
14
+ CLOSED = :closed
15
+ OPEN = :open
16
+ HALF_OPEN = :half_open
17
+
18
+ attr_reader :failure_threshold, :timeout, :expected_errors, :state,
19
+ :failure_count, :last_failure_time, :success_count
20
+
21
+ ##
22
+ # Initialize circuit breaker interceptor
23
+ #
24
+ # @param failure_threshold [Integer] Number of failures before opening circuit (default: 5)
25
+ # @param timeout [Integer] Timeout in seconds before trying half-open (default: 60)
26
+ # @param expected_errors [Array<Class>] Error classes that should trigger circuit breaker
27
+ def initialize(failure_threshold: 5, timeout: 60, expected_errors: nil)
28
+ @failure_threshold = failure_threshold
29
+ @timeout = timeout
30
+ @expected_errors = expected_errors || default_expected_errors
31
+ @state = CLOSED
32
+ @failure_count = 0
33
+ @success_count = 0
34
+ @last_failure_time = nil
35
+ @mutex = Mutex.new
36
+
37
+ validate_configuration!
38
+ end
39
+
40
+ ##
41
+ # Execute request with circuit breaker logic
42
+ #
43
+ # @param request [Object] The request object
44
+ # @param context [Hash] Request context
45
+ # @param next_middleware [Proc] Next middleware in chain
46
+ # @return [Object] Response from next middleware
47
+ def call(request, context, next_middleware)
48
+ @mutex.synchronize do
49
+ case @state
50
+ when CLOSED
51
+ execute_request(request, context, next_middleware)
52
+ when OPEN
53
+ check_timeout_and_execute(request, context, next_middleware)
54
+ when HALF_OPEN
55
+ execute_half_open_request(request, context, next_middleware)
56
+ end
57
+ end
58
+ end
59
+
60
+ ##
61
+ # Get current circuit breaker status
62
+ #
63
+ # @return [Hash] Circuit breaker status information
64
+ def status
65
+ @mutex.synchronize do
66
+ {
67
+ state: @state,
68
+ failure_count: @failure_count,
69
+ success_count: @success_count,
70
+ failure_threshold: @failure_threshold,
71
+ timeout: @timeout,
72
+ last_failure_time: @last_failure_time,
73
+ time_until_half_open: time_until_half_open
74
+ }
75
+ end
76
+ end
77
+
78
+ ##
79
+ # Check if the circuit breaker is open
80
+ #
81
+ # @return [Boolean] True if circuit is open
82
+ def open?
83
+ @state == OPEN
84
+ end
85
+
86
+ ##
87
+ # Check if the circuit breaker is closed
88
+ #
89
+ # @return [Boolean] True if circuit is closed
90
+ def closed?
91
+ @state == CLOSED
92
+ end
93
+
94
+ ##
95
+ # Check if the circuit breaker is half-open
96
+ #
97
+ # @return [Boolean] True if circuit is half-open
98
+ def half_open?
99
+ @state == HALF_OPEN
100
+ end
101
+
102
+ ##
103
+ # Reset the circuit breaker to closed state
104
+ def reset!
105
+ @mutex.synchronize do
106
+ @state = CLOSED
107
+ @failure_count = 0
108
+ @success_count = 0
109
+ @last_failure_time = nil
110
+ end
111
+ end
112
+
113
+ ##
114
+ # Force the circuit breaker to open state
115
+ def trip!
116
+ @mutex.synchronize do
117
+ @state = OPEN
118
+ @last_failure_time = Time.now
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ ##
125
+ # Execute request in closed state
126
+ def execute_request(request, context, next_middleware)
127
+ response = next_middleware.call(request, context)
128
+ on_success
129
+ response
130
+ rescue StandardError => e
131
+ on_failure if circuit_breaker_error?(e)
132
+ raise e
133
+ end
134
+
135
+ ##
136
+ # Check timeout and execute request when circuit is open
137
+ def check_timeout_and_execute(request, context, next_middleware)
138
+ raise A2A::Errors::AgentUnavailable, "Circuit breaker is OPEN. Service unavailable." unless timeout_expired?
139
+
140
+ @state = HALF_OPEN
141
+ @success_count = 0
142
+ execute_half_open_request(request, context, next_middleware)
143
+ end
144
+
145
+ ##
146
+ # Execute request in half-open state
147
+ def execute_half_open_request(request, context, next_middleware)
148
+ response = next_middleware.call(request, context)
149
+ on_half_open_success
150
+ response
151
+ rescue StandardError => e
152
+ on_half_open_failure if circuit_breaker_error?(e)
153
+ raise e
154
+ end
155
+
156
+ ##
157
+ # Handle successful request
158
+ def on_success
159
+ @failure_count = 0
160
+ end
161
+
162
+ ##
163
+ # Handle failed request
164
+ def on_failure
165
+ @failure_count += 1
166
+ @last_failure_time = Time.now
167
+
168
+ return unless @failure_count >= @failure_threshold
169
+
170
+ @state = OPEN
171
+ end
172
+
173
+ ##
174
+ # Handle successful request in half-open state
175
+ def on_half_open_success
176
+ @success_count += 1
177
+
178
+ # Close circuit after successful request in half-open state
179
+ @state = CLOSED
180
+ @failure_count = 0
181
+ @success_count = 0
182
+ end
183
+
184
+ ##
185
+ # Handle failed request in half-open state
186
+ def on_half_open_failure
187
+ @state = OPEN
188
+ @failure_count += 1
189
+ @last_failure_time = Time.now
190
+ end
191
+
192
+ ##
193
+ # Check if timeout has expired for opening circuit
194
+ def timeout_expired?
195
+ return false unless @last_failure_time
196
+
197
+ Time.now - @last_failure_time >= @timeout
198
+ end
199
+
200
+ ##
201
+ # Calculate time until circuit can be half-open
202
+ def time_until_half_open
203
+ return 0 unless @state == OPEN && @last_failure_time
204
+
205
+ elapsed = Time.now - @last_failure_time
206
+ remaining = @timeout - elapsed
207
+ [remaining, 0].max
208
+ end
209
+
210
+ ##
211
+ # Check if error should trigger circuit breaker
212
+ def circuit_breaker_error?(error)
213
+ @expected_errors.any? { |error_class| error.is_a?(error_class) }
214
+ end
215
+
216
+ ##
217
+ # Default error classes that should trigger circuit breaker
218
+ def default_expected_errors
219
+ [
220
+ A2A::Errors::TimeoutError,
221
+ A2A::Errors::HTTPError,
222
+ A2A::Errors::TransportError,
223
+ A2A::Errors::AgentUnavailable,
224
+ A2A::Errors::ResourceExhausted,
225
+ A2A::Errors::InternalError,
226
+ Faraday::TimeoutError,
227
+ Faraday::ConnectionFailed
228
+ ]
229
+ end
230
+
231
+ ##
232
+ # Validate configuration parameters
233
+ def validate_configuration!
234
+ raise ArgumentError, "failure_threshold must be positive" if @failure_threshold <= 0
235
+
236
+ raise ArgumentError, "timeout must be positive" if @timeout <= 0
237
+
238
+ return if @expected_errors.is_a?(Array)
239
+
240
+ raise ArgumentError, "expected_errors must be an array"
241
+ end
242
+ end
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,371 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+ require "json"
5
+
6
+ ##
7
+ # Logging interceptor for request/response debugging
8
+ #
9
+ # Provides comprehensive logging of A2A requests, responses, and errors
10
+ # with configurable log levels and filtering.
11
+ #
12
+ module A2A
13
+ module Client
14
+ module Middleware
15
+ class LoggingInterceptor
16
+ attr_reader :logger, :log_level, :log_requests, :log_responses, :log_errors
17
+
18
+ ##
19
+ # Initialize logging interceptor
20
+ #
21
+ # @param logger [Logger, nil] Logger instance (creates default if nil)
22
+ # @param log_level [Symbol] Log level (:debug, :info, :warn, :error)
23
+ # @param log_requests [Boolean] Whether to log requests (default: true)
24
+ # @param log_responses [Boolean] Whether to log responses (default: true)
25
+ # @param log_errors [Boolean] Whether to log errors (default: true)
26
+ # @param mask_sensitive [Boolean] Whether to mask sensitive data (default: true)
27
+ def initialize(logger: nil, log_level: :info, log_requests: true,
28
+ log_responses: true, log_errors: true, mask_sensitive: true)
29
+ @logger = logger || create_default_logger
30
+ @log_level = log_level
31
+ @log_requests = log_requests
32
+ @log_responses = log_responses
33
+ @log_errors = log_errors
34
+ @mask_sensitive = mask_sensitive
35
+
36
+ validate_configuration!
37
+ end
38
+
39
+ ##
40
+ # Execute request with logging
41
+ #
42
+ # @param request [Object] The request object
43
+ # @param context [Hash] Request context
44
+ # @param next_middleware [Proc] Next middleware in chain
45
+ # @return [Object] Response from next middleware
46
+ def call(request, context, next_middleware)
47
+ request_id = context[:request_id] || generate_request_id
48
+ context[:request_id] = request_id
49
+
50
+ start_time = Time.now
51
+
52
+ log_request(request, context) if @log_requests
53
+
54
+ begin
55
+ response = next_middleware.call(request, context)
56
+
57
+ duration = Time.now - start_time
58
+ log_response(response, context, duration) if @log_responses
59
+
60
+ response
61
+ rescue StandardError => e
62
+ duration = Time.now - start_time
63
+ log_error(e, context, duration) if @log_errors
64
+ raise e
65
+ end
66
+ end
67
+
68
+ ##
69
+ # Log a request
70
+ #
71
+ # @param request [Object] The request object
72
+ # @param context [Hash] Request context
73
+ def log_request(request, context)
74
+ log_data = {
75
+ type: "request",
76
+ request_id: context[:request_id],
77
+ timestamp: Time.now.utc.iso8601,
78
+ method: extract_method(request),
79
+ url: extract_url(request),
80
+ headers: mask_headers(extract_headers(request)),
81
+ body: mask_body(extract_body(request)),
82
+ context: sanitize_context(context)
83
+ }
84
+
85
+ @logger.send(@log_level, format_log_message("A2A Request", log_data))
86
+ end
87
+
88
+ ##
89
+ # Log a response
90
+ #
91
+ # @param response [Object] The response object
92
+ # @param context [Hash] Request context
93
+ # @param duration [Float] Request duration in seconds
94
+ def log_response(response, context, duration)
95
+ log_data = {
96
+ type: "response",
97
+ request_id: context[:request_id],
98
+ timestamp: Time.now.utc.iso8601,
99
+ duration_ms: (duration * 1000).round(2),
100
+ status: extract_status(response),
101
+ headers: mask_headers(extract_response_headers(response)),
102
+ body: mask_body(extract_response_body(response)),
103
+ success: response_successful?(response)
104
+ }
105
+
106
+ level = response_successful?(response) ? @log_level : :warn
107
+ @logger.send(level, format_log_message("A2A Response", log_data))
108
+ end
109
+
110
+ ##
111
+ # Log an error
112
+ #
113
+ # @param error [Exception] The error that occurred
114
+ # @param context [Hash] Request context
115
+ # @param duration [Float] Request duration in seconds
116
+ def log_error(error, context, duration)
117
+ log_data = {
118
+ type: "error",
119
+ request_id: context[:request_id],
120
+ timestamp: Time.now.utc.iso8601,
121
+ duration_ms: (duration * 1000).round(2),
122
+ error_class: error.class.name,
123
+ error_message: error.message,
124
+ error_code: error.respond_to?(:code) ? error.code : nil,
125
+ backtrace: error.backtrace&.first(10),
126
+ retry_attempt: context[:retry_attempt]
127
+ }
128
+
129
+ @logger.error(format_log_message("A2A Error", log_data))
130
+ end
131
+
132
+ private
133
+
134
+ ##
135
+ # Create default logger
136
+ #
137
+ # @return [Logger] Default logger instance
138
+ def create_default_logger
139
+ logger = Logger.new($stdout)
140
+ logger.level = Logger::INFO
141
+ logger.formatter = proc do |severity, datetime, _progname, msg|
142
+ "[#{datetime.strftime('%Y-%m-%d %H:%M:%S')}] #{severity}: #{msg}\n"
143
+ end
144
+ logger
145
+ end
146
+
147
+ ##
148
+ # Generate a unique request ID
149
+ #
150
+ # @return [String] Request ID
151
+ def generate_request_id
152
+ require "securerandom"
153
+ SecureRandom.hex(8)
154
+ end
155
+
156
+ ##
157
+ # Format log message
158
+ #
159
+ # @param title [String] Log message title
160
+ # @param data [Hash] Log data
161
+ # @return [String] Formatted log message
162
+ def format_log_message(title, data)
163
+ "#{title}: #{JSON.pretty_generate(data)}"
164
+ rescue JSON::GeneratorError
165
+ "#{title}: #{data.inspect}"
166
+ end
167
+
168
+ ##
169
+ # Extract method from request
170
+ #
171
+ # @param request [Object] The request object
172
+ # @return [String, nil] HTTP method or JSON-RPC method
173
+ def extract_method(request)
174
+ if request.respond_to?(:method)
175
+ request.method
176
+ elsif request.respond_to?(:[])
177
+ request["method"] || request[:method]
178
+ end
179
+ end
180
+
181
+ ##
182
+ # Extract URL from request
183
+ #
184
+ # @param request [Object] The request object
185
+ # @return [String, nil] Request URL
186
+ def extract_url(request)
187
+ if request.respond_to?(:url)
188
+ request.url
189
+ elsif request.respond_to?(:uri)
190
+ request.uri.to_s
191
+ end
192
+ end
193
+
194
+ ##
195
+ # Extract headers from request
196
+ #
197
+ # @param request [Object] The request object
198
+ # @return [Hash] Request headers
199
+ def extract_headers(request)
200
+ if request.respond_to?(:headers)
201
+ request.headers.to_h
202
+ else
203
+ {}
204
+ end
205
+ end
206
+
207
+ ##
208
+ # Extract body from request
209
+ #
210
+ # @param request [Object] The request object
211
+ # @return [String, Hash, nil] Request body
212
+ def extract_body(request)
213
+ if request.respond_to?(:body)
214
+ body = request.body
215
+ return parse_json_body(body) if body.is_a?(String)
216
+
217
+ body
218
+ elsif request.respond_to?(:to_h)
219
+ request.to_h
220
+ end
221
+ end
222
+
223
+ ##
224
+ # Extract status from response
225
+ #
226
+ # @param response [Object] The response object
227
+ # @return [Integer, String, nil] Response status
228
+ def extract_status(response)
229
+ if response.respond_to?(:status)
230
+ response.status
231
+ elsif response.respond_to?(:[])
232
+ response["status"] || response[:status]
233
+ end
234
+ end
235
+
236
+ ##
237
+ # Extract headers from response
238
+ #
239
+ # @param response [Object] The response object
240
+ # @return [Hash] Response headers
241
+ def extract_response_headers(response)
242
+ if response.respond_to?(:headers)
243
+ response.headers.to_h
244
+ else
245
+ {}
246
+ end
247
+ end
248
+
249
+ ##
250
+ # Extract body from response
251
+ #
252
+ # @param response [Object] The response object
253
+ # @return [String, Hash, nil] Response body
254
+ def extract_response_body(response)
255
+ if response.respond_to?(:body)
256
+ body = response.body
257
+ return parse_json_body(body) if body.is_a?(String)
258
+
259
+ body
260
+ elsif response.respond_to?(:to_h)
261
+ response.to_h
262
+ end
263
+ end
264
+
265
+ ##
266
+ # Check if response was successful
267
+ #
268
+ # @param response [Object] The response object
269
+ # @return [Boolean] True if successful
270
+ def response_successful?(response)
271
+ if response.respond_to?(:success?)
272
+ response.success?
273
+ elsif response.respond_to?(:status)
274
+ (200..299).cover?(response.status)
275
+ elsif response.respond_to?(:[])
276
+ !response["error"] && !response[:error]
277
+ else
278
+ true # Assume success if we can't determine
279
+ end
280
+ end
281
+
282
+ ##
283
+ # Parse JSON body
284
+ #
285
+ # @param body [String] JSON body string
286
+ # @return [Hash, String] Parsed JSON or original string
287
+ def parse_json_body(body)
288
+ JSON.parse(body)
289
+ rescue JSON::ParserError
290
+ body
291
+ end
292
+
293
+ ##
294
+ # Mask sensitive headers
295
+ #
296
+ # @param headers [Hash] Headers to mask
297
+ # @return [Hash] Masked headers
298
+ def mask_headers(headers)
299
+ return headers unless @mask_sensitive
300
+
301
+ masked = headers.dup
302
+
303
+ # Mask authorization headers
304
+ masked.each do |key, value|
305
+ next unless key.to_s.downcase.include?("authorization") ||
306
+ key.to_s.downcase.include?("token") ||
307
+ key.to_s.downcase.include?("key")
308
+
309
+ masked[key] = mask_value(value)
310
+ end
311
+
312
+ masked
313
+ end
314
+
315
+ ##
316
+ # Mask sensitive body content
317
+ #
318
+ # @param body [Object] Body to mask
319
+ # @return [Object] Masked body
320
+ def mask_body(body)
321
+ return body unless @mask_sensitive
322
+ return body unless body.is_a?(Hash)
323
+
324
+ masked = body.dup
325
+
326
+ # Mask common sensitive fields
327
+ %w[password secret token key credential].each do |field|
328
+ masked.each do |k, v|
329
+ masked[k] = mask_value(v) if k.to_s.downcase.include?(field)
330
+ end
331
+ end
332
+
333
+ masked
334
+ end
335
+
336
+ ##
337
+ # Mask a sensitive value
338
+ #
339
+ # @param value [String] Value to mask
340
+ # @return [String] Masked value
341
+ def mask_value(value)
342
+ return "[nil]" if value.nil?
343
+ return "[empty]" if value.to_s.empty?
344
+
345
+ str = value.to_s
346
+ return str if str.length <= 8
347
+
348
+ "#{str[0..3]}#{'*' * (str.length - 8)}#{str[-4..]}"
349
+ end
350
+
351
+ ##
352
+ # Sanitize context for logging
353
+ #
354
+ # @param context [Hash] Context to sanitize
355
+ # @return [Hash] Sanitized context
356
+ def sanitize_context(context)
357
+ context.reject { |k, _v| k.to_s.include?("password") || k.to_s.include?("secret") }
358
+ end
359
+
360
+ ##
361
+ # Validate configuration
362
+ def validate_configuration!
363
+ valid_levels = %i[debug info warn error]
364
+ return if valid_levels.include?(@log_level)
365
+
366
+ raise ArgumentError, "Invalid log level: #{@log_level}. Must be one of: #{valid_levels.join(', ')}"
367
+ end
368
+ end
369
+ end
370
+ end
371
+ end