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,490 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "net/http"
|
4
|
+
require "json"
|
5
|
+
|
6
|
+
module A2A
|
7
|
+
module Monitoring
|
8
|
+
##
|
9
|
+
# Alerting system for A2A monitoring
|
10
|
+
#
|
11
|
+
# Provides configurable alerting based on metrics thresholds,
|
12
|
+
# error rates, and system health indicators.
|
13
|
+
#
|
14
|
+
class Alerting
|
15
|
+
# Alert severities
|
16
|
+
SEVERITY_INFO = :info
|
17
|
+
SEVERITY_WARNING = :warning
|
18
|
+
SEVERITY_ERROR = :error
|
19
|
+
SEVERITY_CRITICAL = :critical
|
20
|
+
|
21
|
+
# Alert states
|
22
|
+
STATE_FIRING = :firing
|
23
|
+
STATE_RESOLVED = :resolved
|
24
|
+
|
25
|
+
attr_reader :rules, :channels, :active_alerts
|
26
|
+
|
27
|
+
##
|
28
|
+
# Initialize alerting system
|
29
|
+
#
|
30
|
+
# @param config [Hash] Alerting configuration
|
31
|
+
def initialize(config = {})
|
32
|
+
@rules = []
|
33
|
+
@channels = []
|
34
|
+
@active_alerts = {}
|
35
|
+
@config = default_config.merge(config)
|
36
|
+
@mutex = Mutex.new
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Add an alert rule
|
41
|
+
#
|
42
|
+
# @param name [String] Rule name
|
43
|
+
# @param metric [String] Metric to monitor
|
44
|
+
# @param condition [Hash] Alert condition
|
45
|
+
# @param severity [Symbol] Alert severity
|
46
|
+
# @param description [String] Alert description
|
47
|
+
# @param tags [Hash] Additional tags
|
48
|
+
def add_rule(name:, metric:, condition:, severity: SEVERITY_WARNING, description: nil, **tags)
|
49
|
+
rule = {
|
50
|
+
name: name,
|
51
|
+
metric: metric,
|
52
|
+
condition: condition,
|
53
|
+
severity: severity,
|
54
|
+
description: description || "Alert for #{metric}",
|
55
|
+
tags: tags,
|
56
|
+
created_at: Time.now,
|
57
|
+
last_evaluated: nil,
|
58
|
+
evaluation_count: 0
|
59
|
+
}
|
60
|
+
|
61
|
+
@mutex.synchronize { @rules << rule }
|
62
|
+
end
|
63
|
+
|
64
|
+
##
|
65
|
+
# Add an alert channel
|
66
|
+
#
|
67
|
+
# @param channel [Object] Alert channel (webhook, email, etc.)
|
68
|
+
def add_channel(channel)
|
69
|
+
@mutex.synchronize { @channels << channel }
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Evaluate all alert rules against current metrics
|
74
|
+
#
|
75
|
+
# @param metrics [Hash] Current metrics snapshot
|
76
|
+
def evaluate_rules(metrics)
|
77
|
+
@mutex.synchronize do
|
78
|
+
@rules.each do |rule|
|
79
|
+
evaluate_rule(rule, metrics)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
##
|
85
|
+
# Get active alerts
|
86
|
+
#
|
87
|
+
# @return [Array<Hash>] Active alerts
|
88
|
+
def get_active_alerts
|
89
|
+
@mutex.synchronize { @active_alerts.values.dup }
|
90
|
+
end
|
91
|
+
|
92
|
+
##
|
93
|
+
# Get alert statistics
|
94
|
+
#
|
95
|
+
# @return [Hash] Alert statistics
|
96
|
+
def statistics
|
97
|
+
@mutex.synchronize do
|
98
|
+
{
|
99
|
+
total_rules: @rules.size,
|
100
|
+
active_alerts: @active_alerts.size,
|
101
|
+
total_channels: @channels.size,
|
102
|
+
alerts_by_severity: count_alerts_by_severity,
|
103
|
+
evaluation_stats: get_evaluation_stats
|
104
|
+
}
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
##
|
109
|
+
# Clear resolved alerts older than specified time
|
110
|
+
#
|
111
|
+
# @param max_age [Integer] Maximum age in seconds (default: 1 hour)
|
112
|
+
def cleanup_resolved_alerts(max_age: 3600)
|
113
|
+
cutoff_time = Time.now - max_age
|
114
|
+
|
115
|
+
@mutex.synchronize do
|
116
|
+
@active_alerts.reject! do |_, alert|
|
117
|
+
alert[:state] == STATE_RESOLVED && alert[:resolved_at] < cutoff_time
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
private
|
123
|
+
|
124
|
+
##
|
125
|
+
# Default configuration
|
126
|
+
#
|
127
|
+
# @return [Hash] Default configuration
|
128
|
+
def default_config
|
129
|
+
{
|
130
|
+
evaluation_interval: 60,
|
131
|
+
alert_timeout: 300,
|
132
|
+
max_alerts: 1000
|
133
|
+
}
|
134
|
+
end
|
135
|
+
|
136
|
+
##
|
137
|
+
# Evaluate a single alert rule
|
138
|
+
#
|
139
|
+
# @param rule [Hash] Alert rule
|
140
|
+
# @param metrics [Hash] Current metrics
|
141
|
+
def evaluate_rule(rule, metrics)
|
142
|
+
rule[:last_evaluated] = Time.now
|
143
|
+
rule[:evaluation_count] += 1
|
144
|
+
|
145
|
+
# Find matching metrics
|
146
|
+
matching_metrics = find_matching_metrics(rule[:metric], metrics)
|
147
|
+
|
148
|
+
matching_metrics.each do |metric_key, metric_data|
|
149
|
+
evaluate_metric_against_rule(rule, metric_key, metric_data)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
##
|
154
|
+
# Find metrics matching the rule pattern
|
155
|
+
#
|
156
|
+
# @param pattern [String] Metric pattern
|
157
|
+
# @param metrics [Hash] All metrics
|
158
|
+
# @return [Hash] Matching metrics
|
159
|
+
def find_matching_metrics(pattern, metrics)
|
160
|
+
if pattern.include?("*")
|
161
|
+
# Pattern matching
|
162
|
+
regex = Regexp.new(pattern.gsub("*", ".*"))
|
163
|
+
metrics.select { |key, _| key.match?(regex) }
|
164
|
+
else
|
165
|
+
# Exact match
|
166
|
+
metric_data = metrics[pattern]
|
167
|
+
metric_data ? { pattern => metric_data } : {}
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
##
|
172
|
+
# Evaluate a specific metric against a rule
|
173
|
+
#
|
174
|
+
# @param rule [Hash] Alert rule
|
175
|
+
# @param metric_key [String] Metric key
|
176
|
+
# @param metric_data [Hash] Metric data
|
177
|
+
def evaluate_metric_against_rule(rule, metric_key, metric_data)
|
178
|
+
alert_key = "#{rule[:name]}_#{metric_key}"
|
179
|
+
condition = rule[:condition]
|
180
|
+
|
181
|
+
# Extract value based on metric type
|
182
|
+
value = extract_metric_value(metric_data, condition[:field])
|
183
|
+
return unless value
|
184
|
+
|
185
|
+
# Evaluate condition
|
186
|
+
should_fire = evaluate_condition(condition, value)
|
187
|
+
|
188
|
+
if should_fire
|
189
|
+
fire_alert(alert_key, rule, metric_key, value)
|
190
|
+
else
|
191
|
+
resolve_alert(alert_key)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
##
|
196
|
+
# Extract value from metric data
|
197
|
+
#
|
198
|
+
# @param metric_data [Hash] Metric data
|
199
|
+
# @param field [String, nil] Specific field to extract
|
200
|
+
# @return [Numeric, nil] Extracted value
|
201
|
+
def extract_metric_value(metric_data, field = nil)
|
202
|
+
case metric_data[:type]
|
203
|
+
when :counter, :gauge
|
204
|
+
metric_data[:value]
|
205
|
+
when :histogram
|
206
|
+
case field
|
207
|
+
when "avg", "average" then metric_data[:avg]
|
208
|
+
when "p95" then metric_data[:p95]
|
209
|
+
when "p99" then metric_data[:p99]
|
210
|
+
when "max" then metric_data[:max]
|
211
|
+
when "min" then metric_data[:min]
|
212
|
+
when "count" then metric_data[:count]
|
213
|
+
else metric_data[:avg] # Default to average
|
214
|
+
end
|
215
|
+
else
|
216
|
+
metric_data[:value]
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
##
|
221
|
+
# Evaluate alert condition
|
222
|
+
#
|
223
|
+
# @param condition [Hash] Alert condition
|
224
|
+
# @param value [Numeric] Current value
|
225
|
+
# @return [Boolean] Whether condition is met
|
226
|
+
def evaluate_condition(condition, value)
|
227
|
+
operator = condition[:operator] || condition[:op]
|
228
|
+
threshold = condition[:threshold]
|
229
|
+
|
230
|
+
case operator.to_s
|
231
|
+
when "gt", ">" then value > threshold
|
232
|
+
when "gte", ">=" then value >= threshold
|
233
|
+
when "lt", "<" then value < threshold
|
234
|
+
when "lte", "<=" then value <= threshold
|
235
|
+
when "eq", "==" then value == threshold
|
236
|
+
when "ne", "!=" then value != threshold
|
237
|
+
else false
|
238
|
+
end
|
239
|
+
end
|
240
|
+
|
241
|
+
##
|
242
|
+
# Fire an alert
|
243
|
+
#
|
244
|
+
# @param alert_key [String] Alert key
|
245
|
+
# @param rule [Hash] Alert rule
|
246
|
+
# @param metric_key [String] Metric key
|
247
|
+
# @param value [Numeric] Current value
|
248
|
+
def fire_alert(alert_key, rule, metric_key, value)
|
249
|
+
existing_alert = @active_alerts[alert_key]
|
250
|
+
|
251
|
+
# Don't fire if already active and within timeout
|
252
|
+
if existing_alert && existing_alert[:state] == STATE_FIRING
|
253
|
+
time_since_fired = Time.now - existing_alert[:fired_at]
|
254
|
+
return if time_since_fired < @config[:alert_timeout]
|
255
|
+
end
|
256
|
+
|
257
|
+
alert = {
|
258
|
+
key: alert_key,
|
259
|
+
rule_name: rule[:name],
|
260
|
+
metric: metric_key,
|
261
|
+
value: value,
|
262
|
+
condition: rule[:condition],
|
263
|
+
severity: rule[:severity],
|
264
|
+
description: rule[:description],
|
265
|
+
tags: rule[:tags],
|
266
|
+
state: STATE_FIRING,
|
267
|
+
fired_at: Time.now,
|
268
|
+
resolved_at: nil
|
269
|
+
}
|
270
|
+
|
271
|
+
@active_alerts[alert_key] = alert
|
272
|
+
send_alert_notification(alert)
|
273
|
+
end
|
274
|
+
|
275
|
+
##
|
276
|
+
# Resolve an alert
|
277
|
+
#
|
278
|
+
# @param alert_key [String] Alert key
|
279
|
+
def resolve_alert(alert_key)
|
280
|
+
alert = @active_alerts[alert_key]
|
281
|
+
return unless alert && alert[:state] == STATE_FIRING
|
282
|
+
|
283
|
+
alert[:state] = STATE_RESOLVED
|
284
|
+
alert[:resolved_at] = Time.now
|
285
|
+
|
286
|
+
send_alert_notification(alert)
|
287
|
+
end
|
288
|
+
|
289
|
+
##
|
290
|
+
# Send alert notification to all channels
|
291
|
+
#
|
292
|
+
# @param alert [Hash] Alert data
|
293
|
+
def send_alert_notification(alert)
|
294
|
+
@channels.each do |channel|
|
295
|
+
channel.send_alert(alert)
|
296
|
+
rescue StandardError => e
|
297
|
+
warn "Failed to send alert via #{channel.class}: #{e.message}"
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
##
|
302
|
+
# Count alerts by severity
|
303
|
+
#
|
304
|
+
# @return [Hash] Alert counts by severity
|
305
|
+
def count_alerts_by_severity
|
306
|
+
counts = Hash.new(0)
|
307
|
+
|
308
|
+
@active_alerts.each_value do |alert|
|
309
|
+
next unless alert[:state] == STATE_FIRING
|
310
|
+
|
311
|
+
counts[alert[:severity]] += 1
|
312
|
+
end
|
313
|
+
|
314
|
+
counts
|
315
|
+
end
|
316
|
+
|
317
|
+
##
|
318
|
+
# Get evaluation statistics
|
319
|
+
#
|
320
|
+
# @return [Hash] Evaluation statistics
|
321
|
+
def get_evaluation_stats
|
322
|
+
return {} if @rules.empty?
|
323
|
+
|
324
|
+
total_evaluations = @rules.sum { |rule| rule[:evaluation_count] }
|
325
|
+
avg_evaluations = total_evaluations.to_f / @rules.size
|
326
|
+
|
327
|
+
{
|
328
|
+
total_evaluations: total_evaluations,
|
329
|
+
average_evaluations_per_rule: avg_evaluations,
|
330
|
+
last_evaluation: @rules.pluck(:last_evaluated).compact.max
|
331
|
+
}
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
##
|
336
|
+
# Webhook alert channel
|
337
|
+
#
|
338
|
+
class WebhookAlertChannel
|
339
|
+
attr_reader :url, :headers
|
340
|
+
|
341
|
+
##
|
342
|
+
# Initialize webhook channel
|
343
|
+
#
|
344
|
+
# @param url [String] Webhook URL
|
345
|
+
# @param headers [Hash] HTTP headers
|
346
|
+
# @param timeout [Integer] Request timeout
|
347
|
+
def initialize(url, headers: {}, timeout: 10)
|
348
|
+
@url = url
|
349
|
+
@headers = headers
|
350
|
+
@timeout = timeout
|
351
|
+
end
|
352
|
+
|
353
|
+
##
|
354
|
+
# Send alert via webhook
|
355
|
+
#
|
356
|
+
# @param alert [Hash] Alert data
|
357
|
+
def send_alert(alert)
|
358
|
+
payload = format_alert_payload(alert)
|
359
|
+
|
360
|
+
uri = URI(@url)
|
361
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
362
|
+
http.use_ssl = uri.scheme == "https"
|
363
|
+
http.read_timeout = @timeout
|
364
|
+
|
365
|
+
request = Net::HTTP::Post.new(uri.path)
|
366
|
+
request["Content-Type"] = "application/json"
|
367
|
+
@headers.each { |key, value| request[key] = value }
|
368
|
+
request.body = payload.to_json
|
369
|
+
|
370
|
+
response = http.request(request)
|
371
|
+
|
372
|
+
return if response.is_a?(Net::HTTPSuccess)
|
373
|
+
|
374
|
+
raise "Webhook request failed: #{response.code} #{response.message}"
|
375
|
+
end
|
376
|
+
|
377
|
+
private
|
378
|
+
|
379
|
+
##
|
380
|
+
# Format alert for webhook payload
|
381
|
+
#
|
382
|
+
# @param alert [Hash] Alert data
|
383
|
+
# @return [Hash] Formatted payload
|
384
|
+
def format_alert_payload(alert)
|
385
|
+
{
|
386
|
+
alert_name: alert[:rule_name],
|
387
|
+
metric: alert[:metric],
|
388
|
+
value: alert[:value],
|
389
|
+
severity: alert[:severity],
|
390
|
+
state: alert[:state],
|
391
|
+
description: alert[:description],
|
392
|
+
fired_at: alert[:fired_at]&.iso8601,
|
393
|
+
resolved_at: alert[:resolved_at]&.iso8601,
|
394
|
+
tags: alert[:tags]
|
395
|
+
}
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
##
|
400
|
+
# Slack alert channel
|
401
|
+
#
|
402
|
+
class SlackAlertChannel
|
403
|
+
##
|
404
|
+
# Initialize Slack channel
|
405
|
+
#
|
406
|
+
# @param webhook_url [String] Slack webhook URL
|
407
|
+
# @param channel [String, nil] Slack channel name
|
408
|
+
# @param username [String] Bot username
|
409
|
+
def initialize(webhook_url, channel: nil, username: "A2A Alerts")
|
410
|
+
@webhook_url = webhook_url
|
411
|
+
@channel = channel
|
412
|
+
@username = username
|
413
|
+
end
|
414
|
+
|
415
|
+
##
|
416
|
+
# Send alert to Slack
|
417
|
+
#
|
418
|
+
# @param alert [Hash] Alert data
|
419
|
+
def send_alert(alert)
|
420
|
+
payload = {
|
421
|
+
username: @username,
|
422
|
+
channel: @channel,
|
423
|
+
attachments: [format_slack_attachment(alert)]
|
424
|
+
}.compact
|
425
|
+
|
426
|
+
uri = URI(@webhook_url)
|
427
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
428
|
+
http.use_ssl = true
|
429
|
+
|
430
|
+
request = Net::HTTP::Post.new(uri.path)
|
431
|
+
request["Content-Type"] = "application/json"
|
432
|
+
request.body = payload.to_json
|
433
|
+
|
434
|
+
response = http.request(request)
|
435
|
+
|
436
|
+
return if response.is_a?(Net::HTTPSuccess)
|
437
|
+
|
438
|
+
raise "Slack webhook request failed: #{response.code}"
|
439
|
+
end
|
440
|
+
|
441
|
+
private
|
442
|
+
|
443
|
+
##
|
444
|
+
# Format alert as Slack attachment
|
445
|
+
#
|
446
|
+
# @param alert [Hash] Alert data
|
447
|
+
# @return [Hash] Slack attachment
|
448
|
+
def format_slack_attachment(alert)
|
449
|
+
color = case alert[:severity]
|
450
|
+
when :critical then "danger"
|
451
|
+
when :error then "danger"
|
452
|
+
when :warning then "warning"
|
453
|
+
else "good"
|
454
|
+
end
|
455
|
+
|
456
|
+
color = "good" if alert[:state] == :resolved
|
457
|
+
|
458
|
+
{
|
459
|
+
color: color,
|
460
|
+
title: "#{alert[:state].to_s.capitalize}: #{alert[:rule_name]}",
|
461
|
+
text: alert[:description],
|
462
|
+
fields: [
|
463
|
+
{
|
464
|
+
title: "Metric",
|
465
|
+
value: alert[:metric],
|
466
|
+
short: true
|
467
|
+
},
|
468
|
+
{
|
469
|
+
title: "Value",
|
470
|
+
value: alert[:value].to_s,
|
471
|
+
short: true
|
472
|
+
},
|
473
|
+
{
|
474
|
+
title: "Severity",
|
475
|
+
value: alert[:severity].to_s.capitalize,
|
476
|
+
short: true
|
477
|
+
},
|
478
|
+
{
|
479
|
+
title: "Time",
|
480
|
+
value: (alert[:fired_at] || alert[:resolved_at]).strftime("%Y-%m-%d %H:%M:%S UTC"),
|
481
|
+
short: true
|
482
|
+
}
|
483
|
+
],
|
484
|
+
footer: "A2A Monitoring",
|
485
|
+
ts: (alert[:fired_at] || alert[:resolved_at]).to_i
|
486
|
+
}
|
487
|
+
end
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|