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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +137 -0
- data/.simplecov +46 -0
- data/.yardopts +10 -0
- data/CHANGELOG.md +33 -0
- data/CODE_OF_CONDUCT.md +128 -0
- data/CONTRIBUTING.md +165 -0
- data/Gemfile +43 -0
- data/Guardfile +34 -0
- data/LICENSE.txt +21 -0
- data/PUBLISHING_CHECKLIST.md +214 -0
- data/README.md +171 -0
- data/Rakefile +165 -0
- data/docs/agent_execution.md +309 -0
- data/docs/api_reference.md +792 -0
- data/docs/configuration.md +780 -0
- data/docs/events.md +475 -0
- data/docs/getting_started.md +668 -0
- data/docs/integration.md +262 -0
- data/docs/server_apps.md +621 -0
- data/docs/troubleshooting.md +765 -0
- data/lib/a2a/client/api_methods.rb +263 -0
- data/lib/a2a/client/auth/api_key.rb +161 -0
- data/lib/a2a/client/auth/interceptor.rb +288 -0
- data/lib/a2a/client/auth/jwt.rb +189 -0
- data/lib/a2a/client/auth/oauth2.rb +146 -0
- data/lib/a2a/client/auth.rb +137 -0
- data/lib/a2a/client/base.rb +316 -0
- data/lib/a2a/client/config.rb +210 -0
- data/lib/a2a/client/connection_pool.rb +233 -0
- data/lib/a2a/client/http_client.rb +524 -0
- data/lib/a2a/client/json_rpc_handler.rb +136 -0
- data/lib/a2a/client/middleware/circuit_breaker_interceptor.rb +245 -0
- data/lib/a2a/client/middleware/logging_interceptor.rb +371 -0
- data/lib/a2a/client/middleware/rate_limit_interceptor.rb +142 -0
- data/lib/a2a/client/middleware/retry_interceptor.rb +161 -0
- data/lib/a2a/client/middleware.rb +116 -0
- data/lib/a2a/client/performance_tracker.rb +60 -0
- data/lib/a2a/configuration/defaults.rb +34 -0
- data/lib/a2a/configuration/environment_loader.rb +76 -0
- data/lib/a2a/configuration/file_loader.rb +115 -0
- data/lib/a2a/configuration/inheritance.rb +101 -0
- data/lib/a2a/configuration/validator.rb +180 -0
- data/lib/a2a/configuration.rb +201 -0
- data/lib/a2a/errors.rb +291 -0
- data/lib/a2a/modules.rb +50 -0
- data/lib/a2a/monitoring/alerting.rb +490 -0
- data/lib/a2a/monitoring/distributed_tracing.rb +398 -0
- data/lib/a2a/monitoring/health_endpoints.rb +204 -0
- data/lib/a2a/monitoring/metrics_collector.rb +438 -0
- data/lib/a2a/monitoring.rb +463 -0
- data/lib/a2a/plugin.rb +358 -0
- data/lib/a2a/plugin_manager.rb +159 -0
- data/lib/a2a/plugins/example_auth.rb +81 -0
- data/lib/a2a/plugins/example_middleware.rb +118 -0
- data/lib/a2a/plugins/example_transport.rb +76 -0
- data/lib/a2a/protocol/agent_card.rb +8 -0
- data/lib/a2a/protocol/agent_card_server.rb +584 -0
- data/lib/a2a/protocol/capability.rb +496 -0
- data/lib/a2a/protocol/json_rpc.rb +254 -0
- data/lib/a2a/protocol/message.rb +8 -0
- data/lib/a2a/protocol/task.rb +8 -0
- data/lib/a2a/rails/a2a_controller.rb +258 -0
- data/lib/a2a/rails/controller_helpers.rb +499 -0
- data/lib/a2a/rails/engine.rb +167 -0
- data/lib/a2a/rails/generators/agent_generator.rb +311 -0
- data/lib/a2a/rails/generators/install_generator.rb +209 -0
- data/lib/a2a/rails/generators/migration_generator.rb +232 -0
- data/lib/a2a/rails/generators/templates/add_a2a_indexes.rb +57 -0
- data/lib/a2a/rails/generators/templates/agent_controller.rb +122 -0
- data/lib/a2a/rails/generators/templates/agent_controller_spec.rb +160 -0
- data/lib/a2a/rails/generators/templates/agent_readme.md +200 -0
- data/lib/a2a/rails/generators/templates/create_a2a_push_notification_configs.rb +68 -0
- data/lib/a2a/rails/generators/templates/create_a2a_tasks.rb +83 -0
- data/lib/a2a/rails/generators/templates/example_agent_controller.rb +228 -0
- data/lib/a2a/rails/generators/templates/initializer.rb +108 -0
- data/lib/a2a/rails/generators/templates/push_notification_config_model.rb +228 -0
- data/lib/a2a/rails/generators/templates/task_model.rb +200 -0
- data/lib/a2a/rails/tasks/a2a.rake +228 -0
- data/lib/a2a/server/a2a_methods.rb +520 -0
- data/lib/a2a/server/agent.rb +537 -0
- data/lib/a2a/server/agent_execution/agent_executor.rb +279 -0
- data/lib/a2a/server/agent_execution/request_context.rb +219 -0
- data/lib/a2a/server/apps/rack_app.rb +311 -0
- data/lib/a2a/server/apps/sinatra_app.rb +261 -0
- data/lib/a2a/server/default_request_handler.rb +350 -0
- data/lib/a2a/server/events/event_consumer.rb +116 -0
- data/lib/a2a/server/events/event_queue.rb +226 -0
- data/lib/a2a/server/example_agent.rb +248 -0
- data/lib/a2a/server/handler.rb +281 -0
- data/lib/a2a/server/middleware/authentication_middleware.rb +212 -0
- data/lib/a2a/server/middleware/cors_middleware.rb +171 -0
- data/lib/a2a/server/middleware/logging_middleware.rb +362 -0
- data/lib/a2a/server/middleware/rate_limit_middleware.rb +382 -0
- data/lib/a2a/server/middleware.rb +213 -0
- data/lib/a2a/server/push_notification_manager.rb +327 -0
- data/lib/a2a/server/request_handler.rb +136 -0
- data/lib/a2a/server/storage/base.rb +141 -0
- data/lib/a2a/server/storage/database.rb +266 -0
- data/lib/a2a/server/storage/memory.rb +274 -0
- data/lib/a2a/server/storage/redis.rb +320 -0
- data/lib/a2a/server/storage.rb +38 -0
- data/lib/a2a/server/task_manager.rb +534 -0
- data/lib/a2a/transport/grpc.rb +481 -0
- data/lib/a2a/transport/http.rb +415 -0
- data/lib/a2a/transport/sse.rb +499 -0
- data/lib/a2a/types/agent_card.rb +540 -0
- data/lib/a2a/types/artifact.rb +99 -0
- data/lib/a2a/types/base_model.rb +223 -0
- data/lib/a2a/types/events.rb +117 -0
- data/lib/a2a/types/message.rb +106 -0
- data/lib/a2a/types/part.rb +288 -0
- data/lib/a2a/types/push_notification.rb +139 -0
- data/lib/a2a/types/security.rb +167 -0
- data/lib/a2a/types/task.rb +154 -0
- data/lib/a2a/types.rb +88 -0
- data/lib/a2a/utils/helpers.rb +245 -0
- data/lib/a2a/utils/message_buffer.rb +278 -0
- data/lib/a2a/utils/performance.rb +247 -0
- data/lib/a2a/utils/rails_detection.rb +97 -0
- data/lib/a2a/utils/structured_logger.rb +306 -0
- data/lib/a2a/utils/time_helpers.rb +167 -0
- data/lib/a2a/utils/validation.rb +8 -0
- data/lib/a2a/version.rb +6 -0
- data/lib/a2a-rails.rb +58 -0
- data/lib/a2a.rb +198 -0
- metadata +437 -0
@@ -0,0 +1,362 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "logger"
|
4
|
+
require "json"
|
5
|
+
require_relative "../../utils/rails_detection"
|
6
|
+
|
7
|
+
##
|
8
|
+
# Logging middleware for A2A requests
|
9
|
+
#
|
10
|
+
# Provides comprehensive logging for A2A requests and responses,
|
11
|
+
# including request/response tracking, performance metrics, and
|
12
|
+
# structured logging support.
|
13
|
+
#
|
14
|
+
# @example Basic usage
|
15
|
+
# middleware = LoggingMiddleware.new(
|
16
|
+
# logger: Rails.logger,
|
17
|
+
# level: :info,
|
18
|
+
# format: :structured
|
19
|
+
# )
|
20
|
+
#
|
21
|
+
module A2A
|
22
|
+
module Server
|
23
|
+
module Middleware
|
24
|
+
class LoggingMiddleware
|
25
|
+
include A2A::Utils::RailsDetection
|
26
|
+
|
27
|
+
attr_accessor :logger
|
28
|
+
attr_reader :level, :format, :options
|
29
|
+
|
30
|
+
# Logging formats
|
31
|
+
FORMATS = %i[simple detailed structured json].freeze
|
32
|
+
|
33
|
+
##
|
34
|
+
# Initialize logging middleware
|
35
|
+
#
|
36
|
+
# @param logger [Logger] The logger instance to use
|
37
|
+
# @param level [Symbol] The log level (:debug, :info, :warn, :error)
|
38
|
+
# @param format [Symbol] The log format (:simple, :detailed, :structured, :json)
|
39
|
+
# @param options [Hash] Additional logging options
|
40
|
+
# @option options [Boolean] :log_params Whether to log request parameters
|
41
|
+
# @option options [Boolean] :log_response Whether to log response data
|
42
|
+
# @option options [Boolean] :log_errors Whether to log error details
|
43
|
+
# @option options [Array<String>] :filtered_params Parameters to filter from logs
|
44
|
+
def initialize(logger: nil, level: :info, format: :detailed, **options)
|
45
|
+
@logger = logger || default_logger
|
46
|
+
@level = level
|
47
|
+
@format = format
|
48
|
+
@options = {
|
49
|
+
log_params: true,
|
50
|
+
log_response: false,
|
51
|
+
log_errors: true,
|
52
|
+
filtered_params: %w[password token api_key secret],
|
53
|
+
include_context: true,
|
54
|
+
include_timing: true
|
55
|
+
}.merge(options)
|
56
|
+
|
57
|
+
validate_format!
|
58
|
+
end
|
59
|
+
|
60
|
+
##
|
61
|
+
# Process logging for a request
|
62
|
+
#
|
63
|
+
# @param request [A2A::Protocol::Request] The JSON-RPC request
|
64
|
+
# @param context [A2A::Server::Context] The request context
|
65
|
+
# @yield Block to continue the middleware chain
|
66
|
+
# @return [Object] The result from the next middleware or handler
|
67
|
+
def call(request, context)
|
68
|
+
start_time = Time.now
|
69
|
+
request_id = generate_request_id(request, context)
|
70
|
+
|
71
|
+
# Log request start
|
72
|
+
log_request_start(request, context, request_id)
|
73
|
+
|
74
|
+
begin
|
75
|
+
# Execute next middleware/handler
|
76
|
+
result = yield
|
77
|
+
|
78
|
+
# Log successful completion
|
79
|
+
duration = Time.now - start_time
|
80
|
+
log_request_success(request, context, result, duration, request_id)
|
81
|
+
|
82
|
+
result
|
83
|
+
rescue StandardError => e
|
84
|
+
# Log error
|
85
|
+
duration = Time.now - start_time
|
86
|
+
log_request_error(request, context, e, duration, request_id)
|
87
|
+
|
88
|
+
# Re-raise the error
|
89
|
+
raise
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
##
|
94
|
+
# Set custom logger
|
95
|
+
#
|
96
|
+
# @param new_logger [Logger] The new logger instance
|
97
|
+
|
98
|
+
##
|
99
|
+
# Add filtered parameter
|
100
|
+
#
|
101
|
+
# @param param [String] Parameter name to filter
|
102
|
+
def add_filtered_param(param)
|
103
|
+
@options[:filtered_params] << param.to_s
|
104
|
+
end
|
105
|
+
|
106
|
+
##
|
107
|
+
# Remove filtered parameter
|
108
|
+
#
|
109
|
+
# @param param [String] Parameter name to unfilter
|
110
|
+
def remove_filtered_param(param)
|
111
|
+
@options[:filtered_params].delete(param.to_s)
|
112
|
+
end
|
113
|
+
|
114
|
+
private
|
115
|
+
|
116
|
+
##
|
117
|
+
# Validate the logging format
|
118
|
+
def validate_format!
|
119
|
+
return if FORMATS.include?(@format)
|
120
|
+
|
121
|
+
raise ArgumentError, "Invalid format: #{@format}. Must be one of: #{FORMATS.join(', ')}"
|
122
|
+
end
|
123
|
+
|
124
|
+
##
|
125
|
+
# Get default logger
|
126
|
+
def default_logger
|
127
|
+
if rails_logger
|
128
|
+
rails_logger
|
129
|
+
else
|
130
|
+
Logger.new($stdout).tap do |logger|
|
131
|
+
logger.level = Logger::INFO
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# Generate a unique request ID
|
138
|
+
#
|
139
|
+
# @param request [A2A::Protocol::Request] The request
|
140
|
+
# @param context [A2A::Server::Context] The context
|
141
|
+
# @return [String] The request ID
|
142
|
+
def generate_request_id(request, _context)
|
143
|
+
# Use existing request ID if available
|
144
|
+
return request.id.to_s if request.id
|
145
|
+
|
146
|
+
# Generate a new ID
|
147
|
+
"req_#{Time.now.to_f}_#{rand(10_000)}"
|
148
|
+
end
|
149
|
+
|
150
|
+
##
|
151
|
+
# Log request start
|
152
|
+
#
|
153
|
+
# @param request [A2A::Protocol::Request] The request
|
154
|
+
# @param context [A2A::Server::Context] The context
|
155
|
+
# @param request_id [String] The request ID
|
156
|
+
def log_request_start(request, context, request_id)
|
157
|
+
case @format
|
158
|
+
when :simple
|
159
|
+
@logger.send(@level, "A2A Request: #{request.method}")
|
160
|
+
when :detailed
|
161
|
+
log_detailed_start(request, context, request_id)
|
162
|
+
when :structured
|
163
|
+
log_structured_start(request, context, request_id)
|
164
|
+
when :json
|
165
|
+
log_json_start(request, context, request_id)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
##
|
170
|
+
# Log successful request completion
|
171
|
+
#
|
172
|
+
# @param request [A2A::Protocol::Request] The request
|
173
|
+
# @param context [A2A::Server::Context] The context
|
174
|
+
# @param result [Object] The response result
|
175
|
+
# @param duration [Float] Request duration in seconds
|
176
|
+
# @param request_id [String] The request ID
|
177
|
+
def log_request_success(request, context, result, duration, request_id)
|
178
|
+
case @format
|
179
|
+
when :simple
|
180
|
+
@logger.send(@level, "A2A Response: #{request.method} (#{duration.round(3)}s)")
|
181
|
+
when :detailed
|
182
|
+
log_detailed_success(request, context, result, duration, request_id)
|
183
|
+
when :structured
|
184
|
+
log_structured_success(request, context, result, duration, request_id)
|
185
|
+
when :json
|
186
|
+
log_json_success(request, context, result, duration, request_id)
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
##
|
191
|
+
# Log request error
|
192
|
+
#
|
193
|
+
# @param request [A2A::Protocol::Request] The request
|
194
|
+
# @param context [A2A::Server::Context] The context
|
195
|
+
# @param error [StandardError] The error
|
196
|
+
# @param duration [Float] Request duration in seconds
|
197
|
+
# @param request_id [String] The request ID
|
198
|
+
def log_request_error(request, context, error, duration, request_id)
|
199
|
+
case @format
|
200
|
+
when :simple
|
201
|
+
@logger.error("A2A Error: #{request.method} - #{error.class.name}: #{error.message}")
|
202
|
+
when :detailed
|
203
|
+
log_detailed_error(request, context, error, duration, request_id)
|
204
|
+
when :structured
|
205
|
+
log_structured_error(request, context, error, duration, request_id)
|
206
|
+
when :json
|
207
|
+
log_json_error(request, context, error, duration, request_id)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
##
|
212
|
+
# Log detailed request start
|
213
|
+
def log_detailed_start(request, context, request_id)
|
214
|
+
message = "A2A Request Started - Method: #{request.method}, ID: #{request_id}"
|
215
|
+
|
216
|
+
if @options[:log_params] && !request.params.empty?
|
217
|
+
filtered_params = filter_sensitive_data(request.params)
|
218
|
+
message += ", Params: #{filtered_params.inspect}"
|
219
|
+
end
|
220
|
+
|
221
|
+
message += ", Authenticated: true" if @options[:include_context] && context.authenticated?
|
222
|
+
|
223
|
+
@logger.send(@level, message)
|
224
|
+
end
|
225
|
+
|
226
|
+
##
|
227
|
+
# Log detailed request success
|
228
|
+
def log_detailed_success(request, _context, result, duration, request_id)
|
229
|
+
message = "A2A Request Completed - Method: #{request.method}, ID: #{request_id}, Duration: #{duration.round(3)}s"
|
230
|
+
|
231
|
+
if @options[:log_response] && result
|
232
|
+
filtered_result = filter_sensitive_data(result)
|
233
|
+
message += ", Response: #{filtered_result.inspect}"
|
234
|
+
end
|
235
|
+
|
236
|
+
@logger.send(@level, message)
|
237
|
+
end
|
238
|
+
|
239
|
+
##
|
240
|
+
# Log detailed request error
|
241
|
+
def log_detailed_error(request, _context, error, duration, request_id)
|
242
|
+
message = "A2A Request Failed - Method: #{request.method}, ID: #{request_id}, Duration: #{duration.round(3)}s"
|
243
|
+
message += ", Error: #{error.class.name}: #{error.message}"
|
244
|
+
|
245
|
+
if @options[:log_errors] && error.respond_to?(:backtrace) && error.backtrace
|
246
|
+
message += ", Backtrace: #{error.backtrace.first(3).join(' | ')}"
|
247
|
+
end
|
248
|
+
|
249
|
+
@logger.error(message)
|
250
|
+
end
|
251
|
+
|
252
|
+
##
|
253
|
+
# Log structured request start
|
254
|
+
def log_structured_start(request, context, request_id)
|
255
|
+
data = {
|
256
|
+
event: "a2a_request_start",
|
257
|
+
method: request.method,
|
258
|
+
request_id: request_id,
|
259
|
+
timestamp: Time.now.iso8601
|
260
|
+
}
|
261
|
+
|
262
|
+
data[:params] = filter_sensitive_data(request.params) if @options[:log_params] && !request.params.empty?
|
263
|
+
|
264
|
+
if @options[:include_context]
|
265
|
+
data[:authenticated] = context.authenticated?
|
266
|
+
data[:user] = context.user if context.user
|
267
|
+
end
|
268
|
+
|
269
|
+
@logger.send(@level, format_structured_log(data))
|
270
|
+
end
|
271
|
+
|
272
|
+
##
|
273
|
+
# Log structured request success
|
274
|
+
def log_structured_success(request, _context, result, duration, request_id)
|
275
|
+
data = {
|
276
|
+
event: "a2a_request_success",
|
277
|
+
method: request.method,
|
278
|
+
request_id: request_id,
|
279
|
+
duration: duration.round(3),
|
280
|
+
timestamp: Time.now.iso8601
|
281
|
+
}
|
282
|
+
|
283
|
+
data[:response] = filter_sensitive_data(result) if @options[:log_response] && result
|
284
|
+
|
285
|
+
@logger.send(@level, format_structured_log(data))
|
286
|
+
end
|
287
|
+
|
288
|
+
##
|
289
|
+
# Log structured request error
|
290
|
+
def log_structured_error(request, _context, error, duration, request_id)
|
291
|
+
data = {
|
292
|
+
event: "a2a_request_error",
|
293
|
+
method: request.method,
|
294
|
+
request_id: request_id,
|
295
|
+
duration: duration.round(3),
|
296
|
+
error_class: error.class.name,
|
297
|
+
error_message: error.message,
|
298
|
+
timestamp: Time.now.iso8601
|
299
|
+
}
|
300
|
+
|
301
|
+
data[:backtrace] = error.backtrace.first(5) if @options[:log_errors] && error.respond_to?(:backtrace) && error.backtrace
|
302
|
+
|
303
|
+
data[:error_code] = error.code if error.respond_to?(:code)
|
304
|
+
|
305
|
+
@logger.error(format_structured_log(data))
|
306
|
+
end
|
307
|
+
|
308
|
+
##
|
309
|
+
# Log JSON format (same as structured but always JSON)
|
310
|
+
def log_json_start(request, context, request_id)
|
311
|
+
log_structured_start(request, context, request_id)
|
312
|
+
end
|
313
|
+
|
314
|
+
def log_json_success(request, context, result, duration, request_id)
|
315
|
+
log_structured_success(request, context, result, duration, request_id)
|
316
|
+
end
|
317
|
+
|
318
|
+
def log_json_error(request, context, error, duration, request_id)
|
319
|
+
log_structured_error(request, context, error, duration, request_id)
|
320
|
+
end
|
321
|
+
|
322
|
+
##
|
323
|
+
# Format structured log data
|
324
|
+
#
|
325
|
+
# @param data [Hash] The log data
|
326
|
+
# @return [String] Formatted log message
|
327
|
+
def format_structured_log(data)
|
328
|
+
if @format == :json
|
329
|
+
JSON.generate(data)
|
330
|
+
else
|
331
|
+
# Key=value format for structured logs
|
332
|
+
data.map { |k, v| "#{k}=#{v.inspect}" }.join(" ")
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
##
|
337
|
+
# Filter sensitive data from parameters/responses
|
338
|
+
#
|
339
|
+
# @param data [Object] The data to filter
|
340
|
+
# @return [Object] Filtered data
|
341
|
+
def filter_sensitive_data(data)
|
342
|
+
case data
|
343
|
+
when Hash
|
344
|
+
filtered = {}
|
345
|
+
data.each do |key, value|
|
346
|
+
filtered[key] = if @options[:filtered_params].include?(key.to_s.downcase)
|
347
|
+
"[FILTERED]"
|
348
|
+
else
|
349
|
+
filter_sensitive_data(value)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
filtered
|
353
|
+
when Array
|
354
|
+
data.map { |item| filter_sensitive_data(item) }
|
355
|
+
else
|
356
|
+
data
|
357
|
+
end
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
end
|