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,398 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module A2A
|
6
|
+
module Monitoring
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
##
|
11
|
+
# Distributed tracing implementation for A2A operations
|
12
|
+
#
|
13
|
+
# Provides OpenTelemetry-compatible distributed tracing to track requests
|
14
|
+
# across service boundaries and identify performance bottlenecks.
|
15
|
+
#
|
16
|
+
module A2A
|
17
|
+
module Monitoring
|
18
|
+
class DistributedTracing
|
19
|
+
# Trace context headers
|
20
|
+
TRACE_PARENT_HEADER = "traceparent"
|
21
|
+
TRACE_STATE_HEADER = "tracestate"
|
22
|
+
|
23
|
+
# Span kinds
|
24
|
+
SPAN_KIND_CLIENT = "client"
|
25
|
+
SPAN_KIND_SERVER = "server"
|
26
|
+
SPAN_KIND_INTERNAL = "internal"
|
27
|
+
|
28
|
+
class << self
|
29
|
+
attr_accessor :tracer, :enabled
|
30
|
+
|
31
|
+
##
|
32
|
+
# Initialize distributed tracing
|
33
|
+
#
|
34
|
+
# @param tracer [Object] OpenTelemetry tracer instance
|
35
|
+
# @param enabled [Boolean] Whether tracing is enabled
|
36
|
+
def initialize!(tracer: nil, enabled: true)
|
37
|
+
@tracer = tracer
|
38
|
+
@enabled = enabled
|
39
|
+
end
|
40
|
+
|
41
|
+
##
|
42
|
+
# Start a new span
|
43
|
+
#
|
44
|
+
# @param name [String] Span name
|
45
|
+
# @param kind [String] Span kind
|
46
|
+
# @param parent [Span, nil] Parent span
|
47
|
+
# @param attributes [Hash] Span attributes
|
48
|
+
# @yield [span] Block to execute within span
|
49
|
+
# @return [Object] Result of the block
|
50
|
+
def trace(name, kind: SPAN_KIND_INTERNAL, parent: nil, **attributes)
|
51
|
+
return yield(NoOpSpan.new) unless @enabled
|
52
|
+
|
53
|
+
span = start_span(name, kind: kind, parent: parent, **attributes)
|
54
|
+
|
55
|
+
begin
|
56
|
+
result = yield(span)
|
57
|
+
span.set_status(:ok)
|
58
|
+
result
|
59
|
+
rescue StandardError => e
|
60
|
+
span.set_status(:error, description: e.message)
|
61
|
+
span.record_exception(e)
|
62
|
+
raise
|
63
|
+
ensure
|
64
|
+
span.finish
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
##
|
69
|
+
# Start a span without automatic finishing
|
70
|
+
#
|
71
|
+
# @param name [String] Span name
|
72
|
+
# @param kind [String] Span kind
|
73
|
+
# @param parent [Span, nil] Parent span
|
74
|
+
# @param attributes [Hash] Span attributes
|
75
|
+
# @return [Span] Started span
|
76
|
+
def start_span(name, kind: SPAN_KIND_INTERNAL, parent: nil, **attributes)
|
77
|
+
return NoOpSpan.new unless @enabled
|
78
|
+
|
79
|
+
if @tracer.respond_to?(:start_span)
|
80
|
+
# Use OpenTelemetry tracer if available
|
81
|
+
@tracer.start_span(name, kind: kind, parent: parent, attributes: attributes)
|
82
|
+
else
|
83
|
+
# Use built-in span implementation
|
84
|
+
Span.new(name, kind: kind, parent: parent, **attributes)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
##
|
89
|
+
# Extract trace context from headers
|
90
|
+
#
|
91
|
+
# @param headers [Hash] HTTP headers
|
92
|
+
# @return [TraceContext, nil] Extracted trace context
|
93
|
+
def extract_context(headers)
|
94
|
+
return nil unless @enabled
|
95
|
+
|
96
|
+
traceparent = headers[TRACE_PARENT_HEADER] || headers[TRACE_PARENT_HEADER.upcase]
|
97
|
+
tracestate = headers[TRACE_STATE_HEADER] || headers[TRACE_STATE_HEADER.upcase]
|
98
|
+
|
99
|
+
return nil unless traceparent
|
100
|
+
|
101
|
+
TraceContext.parse(traceparent, tracestate)
|
102
|
+
end
|
103
|
+
|
104
|
+
##
|
105
|
+
# Inject trace context into headers
|
106
|
+
#
|
107
|
+
# @param headers [Hash] HTTP headers to modify
|
108
|
+
# @param context [TraceContext] Trace context to inject
|
109
|
+
def inject_context(headers, context)
|
110
|
+
return unless @enabled && context
|
111
|
+
|
112
|
+
headers[TRACE_PARENT_HEADER] = context.to_traceparent
|
113
|
+
headers[TRACE_STATE_HEADER] = context.tracestate if context.tracestate
|
114
|
+
end
|
115
|
+
|
116
|
+
##
|
117
|
+
# Get current span from context
|
118
|
+
#
|
119
|
+
# @return [Span, nil] Current active span
|
120
|
+
def current_span
|
121
|
+
return nil unless @enabled
|
122
|
+
|
123
|
+
Thread.current[:a2a_current_span]
|
124
|
+
end
|
125
|
+
|
126
|
+
##
|
127
|
+
# Set current span in context
|
128
|
+
#
|
129
|
+
# @param span [Span] Span to set as current
|
130
|
+
def set_current_span(span)
|
131
|
+
Thread.current[:a2a_current_span] = span
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
##
|
136
|
+
# Trace context for distributed tracing
|
137
|
+
#
|
138
|
+
class TraceContext
|
139
|
+
attr_reader :trace_id, :span_id, :trace_flags, :tracestate
|
140
|
+
|
141
|
+
##
|
142
|
+
# Initialize trace context
|
143
|
+
#
|
144
|
+
# @param trace_id [String] Trace ID (32 hex characters)
|
145
|
+
# @param span_id [String] Span ID (16 hex characters)
|
146
|
+
# @param trace_flags [Integer] Trace flags
|
147
|
+
# @param tracestate [String, nil] Trace state
|
148
|
+
def initialize(trace_id:, span_id:, trace_flags: 1, tracestate: nil)
|
149
|
+
@trace_id = trace_id
|
150
|
+
@span_id = span_id
|
151
|
+
@trace_flags = trace_flags
|
152
|
+
@tracestate = tracestate
|
153
|
+
end
|
154
|
+
|
155
|
+
##
|
156
|
+
# Parse trace context from traceparent header
|
157
|
+
#
|
158
|
+
# @param traceparent [String] Traceparent header value
|
159
|
+
# @param tracestate [String, nil] Tracestate header value
|
160
|
+
# @return [TraceContext, nil] Parsed trace context
|
161
|
+
def self.parse(traceparent, tracestate = nil)
|
162
|
+
# Format: 00-{trace_id}-{span_id}-{trace_flags}
|
163
|
+
parts = traceparent.split("-")
|
164
|
+
return nil unless parts.size == 4 && parts[0] == "00"
|
165
|
+
|
166
|
+
new(
|
167
|
+
trace_id: parts[1],
|
168
|
+
span_id: parts[2],
|
169
|
+
trace_flags: parts[3].to_i(16),
|
170
|
+
tracestate: tracestate
|
171
|
+
)
|
172
|
+
rescue StandardError
|
173
|
+
nil
|
174
|
+
end
|
175
|
+
|
176
|
+
##
|
177
|
+
# Convert to traceparent header format
|
178
|
+
#
|
179
|
+
# @return [String] Traceparent header value
|
180
|
+
def to_traceparent
|
181
|
+
"00-#{@trace_id}-#{@span_id}-#{@trace_flags.to_s(16).rjust(2, '0')}"
|
182
|
+
end
|
183
|
+
|
184
|
+
##
|
185
|
+
# Create child context with new span ID
|
186
|
+
#
|
187
|
+
# @return [TraceContext] Child trace context
|
188
|
+
def create_child
|
189
|
+
self.class.new(
|
190
|
+
trace_id: @trace_id,
|
191
|
+
span_id: generate_span_id,
|
192
|
+
trace_flags: @trace_flags,
|
193
|
+
tracestate: @tracestate
|
194
|
+
)
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
##
|
200
|
+
# Generate a new span ID
|
201
|
+
#
|
202
|
+
# @return [String] 16-character hex span ID
|
203
|
+
def generate_span_id
|
204
|
+
SecureRandom.hex(8)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
##
|
209
|
+
# Span implementation for distributed tracing
|
210
|
+
#
|
211
|
+
class Span
|
212
|
+
attr_reader :name, :kind, :trace_id, :span_id, :parent_span_id, :start_time, :end_time, :attributes, :events,
|
213
|
+
:status
|
214
|
+
|
215
|
+
##
|
216
|
+
# Initialize a new span
|
217
|
+
#
|
218
|
+
# @param name [String] Span name
|
219
|
+
# @param kind [String] Span kind
|
220
|
+
# @param parent [Span, nil] Parent span
|
221
|
+
# @param attributes [Hash] Initial attributes
|
222
|
+
def initialize(name, kind: SPAN_KIND_INTERNAL, parent: nil, **attributes)
|
223
|
+
@name = name
|
224
|
+
@kind = kind
|
225
|
+
@trace_id = parent&.trace_id || generate_trace_id
|
226
|
+
@span_id = generate_span_id
|
227
|
+
@parent_span_id = parent&.span_id
|
228
|
+
@start_time = Time.now
|
229
|
+
@end_time = nil
|
230
|
+
@attributes = attributes
|
231
|
+
@events = []
|
232
|
+
@status = { code: :unset }
|
233
|
+
|
234
|
+
# Set as current span
|
235
|
+
DistributedTracing.set_current_span(self)
|
236
|
+
end
|
237
|
+
|
238
|
+
##
|
239
|
+
# Set span attribute
|
240
|
+
#
|
241
|
+
# @param key [String, Symbol] Attribute key
|
242
|
+
# @param value [Object] Attribute value
|
243
|
+
def set_attribute(key, value)
|
244
|
+
@attributes[key.to_s] = value
|
245
|
+
end
|
246
|
+
|
247
|
+
##
|
248
|
+
# Set multiple attributes
|
249
|
+
#
|
250
|
+
# @param attributes [Hash] Attributes to set
|
251
|
+
def set_attributes(**attributes)
|
252
|
+
@attributes.merge!(attributes.transform_keys(&:to_s))
|
253
|
+
end
|
254
|
+
|
255
|
+
##
|
256
|
+
# Add an event to the span
|
257
|
+
#
|
258
|
+
# @param name [String] Event name
|
259
|
+
# @param attributes [Hash] Event attributes
|
260
|
+
# @param timestamp [Time] Event timestamp
|
261
|
+
def add_event(name, attributes: {}, timestamp: Time.now)
|
262
|
+
@events << {
|
263
|
+
name: name,
|
264
|
+
attributes: attributes,
|
265
|
+
timestamp: timestamp
|
266
|
+
}
|
267
|
+
end
|
268
|
+
|
269
|
+
##
|
270
|
+
# Record an exception
|
271
|
+
#
|
272
|
+
# @param exception [Exception] Exception to record
|
273
|
+
def record_exception(exception)
|
274
|
+
add_event("exception", attributes: {
|
275
|
+
"exception.type" => exception.class.name,
|
276
|
+
"exception.message" => exception.message,
|
277
|
+
"exception.stacktrace" => exception.backtrace&.join("\n")
|
278
|
+
})
|
279
|
+
end
|
280
|
+
|
281
|
+
##
|
282
|
+
# Set span status
|
283
|
+
#
|
284
|
+
# @param code [Symbol] Status code (:ok, :error, :unset)
|
285
|
+
# @param description [String, nil] Status description
|
286
|
+
def set_status(code, description: nil)
|
287
|
+
@status = { code: code, description: description }.compact
|
288
|
+
end
|
289
|
+
|
290
|
+
##
|
291
|
+
# Finish the span
|
292
|
+
#
|
293
|
+
def finish
|
294
|
+
@end_time = Time.now
|
295
|
+
|
296
|
+
# Clear from current context if this is the current span
|
297
|
+
DistributedTracing.set_current_span(@parent) if DistributedTracing.current_span == self
|
298
|
+
|
299
|
+
# Export span if exporter is available
|
300
|
+
export_span
|
301
|
+
end
|
302
|
+
|
303
|
+
##
|
304
|
+
# Get span duration in milliseconds
|
305
|
+
#
|
306
|
+
# @return [Float, nil] Duration in milliseconds
|
307
|
+
def duration_ms
|
308
|
+
return nil unless @end_time
|
309
|
+
|
310
|
+
(@end_time - @start_time) * 1000
|
311
|
+
end
|
312
|
+
|
313
|
+
##
|
314
|
+
# Convert span to hash representation
|
315
|
+
#
|
316
|
+
# @return [Hash] Span data
|
317
|
+
def to_h
|
318
|
+
{
|
319
|
+
name: @name,
|
320
|
+
kind: @kind,
|
321
|
+
trace_id: @trace_id,
|
322
|
+
span_id: @span_id,
|
323
|
+
parent_span_id: @parent_span_id,
|
324
|
+
start_time: @start_time.to_f,
|
325
|
+
end_time: @end_time&.to_f,
|
326
|
+
duration_ms: duration_ms,
|
327
|
+
attributes: @attributes,
|
328
|
+
events: @events,
|
329
|
+
status: @status
|
330
|
+
}.compact
|
331
|
+
end
|
332
|
+
|
333
|
+
##
|
334
|
+
# Get trace context for this span
|
335
|
+
#
|
336
|
+
# @return [TraceContext] Trace context
|
337
|
+
def trace_context
|
338
|
+
TraceContext.new(
|
339
|
+
trace_id: @trace_id,
|
340
|
+
span_id: @span_id,
|
341
|
+
trace_flags: 1
|
342
|
+
)
|
343
|
+
end
|
344
|
+
|
345
|
+
private
|
346
|
+
|
347
|
+
##
|
348
|
+
# Generate a new trace ID
|
349
|
+
#
|
350
|
+
# @return [String] 32-character hex trace ID
|
351
|
+
def generate_trace_id
|
352
|
+
SecureRandom.hex(16)
|
353
|
+
end
|
354
|
+
|
355
|
+
##
|
356
|
+
# Generate a new span ID
|
357
|
+
#
|
358
|
+
# @return [String] 16-character hex span ID
|
359
|
+
def generate_span_id
|
360
|
+
SecureRandom.hex(8)
|
361
|
+
end
|
362
|
+
|
363
|
+
##
|
364
|
+
# Export span to configured exporters
|
365
|
+
#
|
366
|
+
def export_span
|
367
|
+
# This would integrate with OpenTelemetry exporters
|
368
|
+
# For now, just log the span if debugging is enabled
|
369
|
+
return unless A2A.configuration.debug_tracing
|
370
|
+
|
371
|
+
logger = if defined?(::Rails) && ::Rails.respond_to?(:logger) && ::Rails.logger
|
372
|
+
::Rails.logger
|
373
|
+
else
|
374
|
+
require "logger"
|
375
|
+
Logger.new($stdout)
|
376
|
+
end
|
377
|
+
logger.debug { "Span: #{to_h.to_json}" }
|
378
|
+
end
|
379
|
+
end
|
380
|
+
|
381
|
+
##
|
382
|
+
# No-op span for when tracing is disabled
|
383
|
+
#
|
384
|
+
class NoOpSpan
|
385
|
+
def set_attribute(key, value); end
|
386
|
+
def set_attributes(**attributes); end
|
387
|
+
def add_event(name, **options); end
|
388
|
+
def record_exception(exception); end
|
389
|
+
def set_status(code, **options); end
|
390
|
+
def finish; end
|
391
|
+
|
392
|
+
def trace_context
|
393
|
+
nil
|
394
|
+
end
|
395
|
+
end
|
396
|
+
end
|
397
|
+
end
|
398
|
+
end
|
@@ -0,0 +1,204 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module A2A
|
4
|
+
module Monitoring
|
5
|
+
##
|
6
|
+
# Health check endpoints for A2A applications
|
7
|
+
#
|
8
|
+
# Provides standard health check endpoints that can be used
|
9
|
+
# by load balancers, monitoring systems, and orchestrators.
|
10
|
+
#
|
11
|
+
class HealthEndpoints
|
12
|
+
# Initialize health endpoints
|
13
|
+
# @param health_checker [A2A::Monitoring::HealthChecker] Health checker instance
|
14
|
+
def initialize(health_checker = A2A::Monitoring.health_checks)
|
15
|
+
@health_checker = health_checker
|
16
|
+
setup_default_checks
|
17
|
+
end
|
18
|
+
|
19
|
+
# Handle health check request
|
20
|
+
# @param request [Hash] HTTP request data
|
21
|
+
# @return [Array] Rack response array
|
22
|
+
def call(env)
|
23
|
+
path = env["PATH_INFO"]
|
24
|
+
|
25
|
+
case path
|
26
|
+
when "/health"
|
27
|
+
handle_health_check
|
28
|
+
when "/health/ready"
|
29
|
+
handle_readiness_check
|
30
|
+
when "/health/live"
|
31
|
+
handle_liveness_check
|
32
|
+
when "/metrics"
|
33
|
+
handle_metrics_endpoint
|
34
|
+
else
|
35
|
+
[404, { "Content-Type" => "application/json" }, ['{"error":"Not Found"}']]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
# Handle general health check
|
42
|
+
def handle_health_check
|
43
|
+
health_result = @health_checker.check_health
|
44
|
+
status_code = health_result[:status] == :healthy ? 200 : 503
|
45
|
+
|
46
|
+
[status_code, json_headers, [health_result.to_json]]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Handle readiness check (can serve traffic)
|
50
|
+
def handle_readiness_check
|
51
|
+
# Check if application is ready to serve requests
|
52
|
+
ready = check_readiness
|
53
|
+
status_code = ready ? 200 : 503
|
54
|
+
|
55
|
+
response = {
|
56
|
+
status: ready ? "ready" : "not_ready",
|
57
|
+
timestamp: Time.now.iso8601
|
58
|
+
}
|
59
|
+
|
60
|
+
[status_code, json_headers, [response.to_json]]
|
61
|
+
end
|
62
|
+
|
63
|
+
# Handle liveness check (application is running)
|
64
|
+
def handle_liveness_check
|
65
|
+
# Simple liveness check - if we can respond, we're alive
|
66
|
+
response = {
|
67
|
+
status: "alive",
|
68
|
+
timestamp: Time.now.iso8601,
|
69
|
+
uptime: Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
70
|
+
}
|
71
|
+
|
72
|
+
[200, json_headers, [response.to_json]]
|
73
|
+
end
|
74
|
+
|
75
|
+
# Handle metrics endpoint
|
76
|
+
def handle_metrics_endpoint
|
77
|
+
if defined?(Prometheus)
|
78
|
+
# Return Prometheus metrics format
|
79
|
+
metrics = Prometheus::Client.registry.metrics
|
80
|
+
prometheus_output = Prometheus::Client::Formats::Text.marshal(metrics)
|
81
|
+
[200, { "Content-Type" => "text/plain" }, [prometheus_output]]
|
82
|
+
else
|
83
|
+
# Return JSON metrics
|
84
|
+
metrics = A2A::Monitoring.metrics.current_metrics
|
85
|
+
[200, json_headers, [{ metrics: metrics }.to_json]]
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def json_headers
|
90
|
+
{ "Content-Type" => "application/json" }
|
91
|
+
end
|
92
|
+
|
93
|
+
def check_readiness
|
94
|
+
# Check database connectivity if using database storage
|
95
|
+
return false unless check_database_connection
|
96
|
+
|
97
|
+
# Check Redis connectivity if using Redis
|
98
|
+
return false unless check_redis_connection
|
99
|
+
|
100
|
+
# Check plugin system
|
101
|
+
return false unless check_plugins_loaded
|
102
|
+
|
103
|
+
true
|
104
|
+
end
|
105
|
+
|
106
|
+
def check_database_connection
|
107
|
+
return true unless A2A.config.rails_integration
|
108
|
+
|
109
|
+
if defined?(ActiveRecord)
|
110
|
+
ActiveRecord::Base.connection.active?
|
111
|
+
else
|
112
|
+
true
|
113
|
+
end
|
114
|
+
rescue StandardError
|
115
|
+
false
|
116
|
+
end
|
117
|
+
|
118
|
+
def check_redis_connection
|
119
|
+
redis_config = A2A.config.redis_config
|
120
|
+
return true unless redis_config && redis_config[:url]
|
121
|
+
|
122
|
+
if defined?(Redis)
|
123
|
+
redis = Redis.new(url: redis_config[:url])
|
124
|
+
redis.ping == "PONG"
|
125
|
+
else
|
126
|
+
true
|
127
|
+
end
|
128
|
+
rescue StandardError
|
129
|
+
false
|
130
|
+
end
|
131
|
+
|
132
|
+
def check_plugins_loaded
|
133
|
+
# Check if critical plugins are loaded
|
134
|
+
critical_plugins = A2A.config.get(:critical_plugins) || []
|
135
|
+
|
136
|
+
critical_plugins.all? do |plugin_name|
|
137
|
+
A2A::Plugin.loaded?(plugin_name)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def setup_default_checks
|
142
|
+
# Register default health checks
|
143
|
+
@health_checker.register_check(:configuration) do
|
144
|
+
A2A.config.validate!
|
145
|
+
{ status: :healthy, message: "Configuration is valid" }
|
146
|
+
rescue StandardError => e
|
147
|
+
{ status: :unhealthy, message: "Configuration error: #{e.message}" }
|
148
|
+
end
|
149
|
+
|
150
|
+
@health_checker.register_check(:memory_usage) do
|
151
|
+
# Check memory usage (basic check)
|
152
|
+
if defined?(GC)
|
153
|
+
stat = GC.stat
|
154
|
+
heap_used = stat[:heap_allocated_pages] * stat[:heap_page_size]
|
155
|
+
|
156
|
+
# Simple threshold check (adjust as needed)
|
157
|
+
if heap_used > 500_000_000 # 500MB
|
158
|
+
{ status: :warning, message: "High memory usage: #{heap_used} bytes" }
|
159
|
+
else
|
160
|
+
{ status: :healthy, message: "Memory usage: #{heap_used} bytes" }
|
161
|
+
end
|
162
|
+
else
|
163
|
+
{ status: :healthy, message: "Memory check not available" }
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
@health_checker.register_check(:plugin_system) do
|
168
|
+
loaded_count = A2A::Plugin.loaded_plugins.size
|
169
|
+
registered_count = A2A::Plugin.registry.size
|
170
|
+
|
171
|
+
{
|
172
|
+
status: :healthy,
|
173
|
+
message: "Plugins: #{loaded_count}/#{registered_count} loaded"
|
174
|
+
}
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
##
|
180
|
+
# Rack middleware for health endpoints
|
181
|
+
#
|
182
|
+
class HealthMiddleware
|
183
|
+
def initialize(app, health_endpoints = nil)
|
184
|
+
@app = app
|
185
|
+
@health_endpoints = health_endpoints || HealthEndpoints.new
|
186
|
+
end
|
187
|
+
|
188
|
+
def call(env)
|
189
|
+
# Check if this is a health endpoint request
|
190
|
+
if health_endpoint?(env["PATH_INFO"])
|
191
|
+
@health_endpoints.call(env)
|
192
|
+
else
|
193
|
+
@app.call(env)
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
private
|
198
|
+
|
199
|
+
def health_endpoint?(path)
|
200
|
+
path&.start_with?("/health") || path == "/metrics"
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|