e11y 0.2.0 → 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 +4 -4
- data/.rubocop.yml +130 -10
- data/CHANGELOG.md +56 -1
- data/CLAUDE.md +168 -0
- data/CONTRIBUTING.md +640 -0
- data/README.md +134 -702
- data/RELEASE.md +18 -3
- data/Rakefile +108 -29
- data/config/README.md +1 -1
- data/config/loki-local-config.yaml +12 -0
- data/config/otel-collector-config.yaml +44 -0
- data/cucumber.yml +1 -0
- data/docker-compose.yml +18 -2
- data/docs/ADAPTERS.md +76 -0
- data/docs/ADAPTIVE_SAMPLING.md +59 -0
- data/docs/COMPARISON.md +104 -0
- data/docs/CONFIGURATION.md +52 -0
- data/docs/DISTRIBUTED_TRACING.md +44 -0
- data/docs/LIMITATIONS.md +13 -0
- data/docs/METRICS_DSL.md +84 -0
- data/docs/PERFORMANCE.md +60 -0
- data/docs/PII_FILTERING.md +40 -0
- data/docs/PRESETS.md +65 -0
- data/docs/QUICK-START.md +546 -587
- data/docs/RAILS_INTEGRATION.md +29 -0
- data/docs/SCHEMA_VALIDATION.md +63 -0
- data/docs/SLO-PROMQL-ALERTS.md +161 -0
- data/docs/TESTING.md +69 -0
- data/docs/{ADR-001-architecture.md → architecture/ADR-001-architecture.md} +35 -64
- data/docs/{ADR-002-metrics-yabeda.md → architecture/ADR-002-metrics-yabeda.md} +62 -236
- data/docs/{ADR-003-slo-observability.md → architecture/ADR-003-slo-observability.md} +27 -466
- data/docs/{ADR-004-adapter-architecture.md → architecture/ADR-004-adapter-architecture.md} +163 -146
- data/docs/{ADR-005-tracing-context.md → architecture/ADR-005-tracing-context.md} +10 -9
- data/docs/{ADR-006-security-compliance.md → architecture/ADR-006-security-compliance.md} +184 -191
- data/docs/{ADR-007-opentelemetry-integration.md → architecture/ADR-007-opentelemetry-integration.md} +3 -21
- data/docs/{ADR-008-rails-integration.md → architecture/ADR-008-rails-integration.md} +209 -339
- data/docs/{ADR-009-cost-optimization.md → architecture/ADR-009-cost-optimization.md} +45 -54
- data/docs/architecture/ADR-010-developer-experience.md +522 -0
- data/docs/{ADR-011-testing-strategy.md → architecture/ADR-011-testing-strategy.md} +41 -83
- data/docs/{ADR-013-reliability-error-handling.md → architecture/ADR-013-reliability-error-handling.md} +37 -12
- data/docs/{ADR-014-event-driven-slo.md → architecture/ADR-014-event-driven-slo.md} +12 -24
- data/docs/{ADR-015-middleware-order.md → architecture/ADR-015-middleware-order.md} +23 -41
- data/docs/{ADR-016-self-monitoring-slo.md → architecture/ADR-016-self-monitoring-slo.md} +52 -349
- data/docs/{ADR-017-multi-rails-compatibility.md → architecture/ADR-017-multi-rails-compatibility.md} +4 -11
- data/docs/architecture/ADR-018-memory-optimization.md +366 -0
- data/docs/{ADR-INDEX.md → architecture/ADR-INDEX.md} +11 -6
- data/docs/{00-ICP-AND-TIMELINE.md → prd/00-ICP-AND-TIMELINE.md} +6 -6
- data/docs/{01-SCALE-REQUIREMENTS.md → prd/01-SCALE-REQUIREMENTS.md} +6 -6
- data/docs/prd/01-overview-vision.md +19 -14
- data/docs/use_cases/README.md +22 -23
- data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +50 -44
- data/docs/use_cases/UC-002-business-event-tracking.md +26 -95
- data/docs/use_cases/UC-003-event-metrics.md +66 -0
- data/docs/use_cases/UC-004-zero-config-slo-tracking.md +42 -101
- data/docs/use_cases/UC-005-sentry-integration.md +13 -15
- data/docs/use_cases/UC-006-trace-context-management.md +30 -28
- data/docs/use_cases/UC-007-pii-filtering.md +35 -87
- data/docs/use_cases/UC-008-opentelemetry-integration.md +51 -89
- data/docs/use_cases/UC-009-multi-service-tracing.md +4 -4
- data/docs/use_cases/UC-010-background-job-tracking.md +5 -5
- data/docs/use_cases/UC-011-rate-limiting.md +95 -168
- data/docs/use_cases/UC-012-audit-trail.md +21 -46
- data/docs/use_cases/UC-013-high-cardinality-protection.md +29 -167
- data/docs/use_cases/UC-014-adaptive-sampling.md +2 -2
- data/docs/use_cases/UC-015-cost-optimization.md +46 -99
- data/docs/use_cases/UC-016-rails-logger-migration.md +39 -213
- data/docs/use_cases/UC-017-local-development.md +203 -777
- data/docs/use_cases/UC-018-testing-events.md +3 -3
- data/docs/use_cases/UC-019-retention-based-routing.md +53 -106
- data/docs/use_cases/UC-020-event-versioning.md +8 -9
- data/docs/use_cases/UC-021-error-handling-retry-dlq.md +18 -22
- data/docs/use_cases/UC-022-event-registry.md +15 -21
- data/docs/use_cases/backlog.md +119 -87
- data/e11y.gemspec +2 -2
- data/gems/e11y-devtools/README.md +136 -0
- data/gems/e11y-devtools/config/routes.rb +8 -0
- data/gems/e11y-devtools/e11y-devtools.gemspec +25 -0
- data/gems/e11y-devtools/exe/e11y +34 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/server.rb +96 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tool_base.rb +25 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/clear.rb +31 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/errors.rb +35 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/event_detail.rb +33 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/events_by_trace.rb +33 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/interactions.rb +40 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/recent_events.rb +34 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/search.rb +34 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/stats.rb +30 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/assets/overlay.js +115 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/controller.rb +54 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/engine.rb +26 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/middleware.rb +80 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/rails_controller.rb +42 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/app.rb +262 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/grouping.rb +66 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_detail.rb +62 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_list.rb +70 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/interaction_list.rb +47 -0
- data/gems/e11y-devtools/lib/e11y/devtools/version.rb +8 -0
- data/gems/e11y-devtools/lib/e11y/devtools.rb +13 -0
- data/gems/e11y-devtools/spec/e11y/devtools/mcp/tools_spec.rb +107 -0
- data/gems/e11y-devtools/spec/e11y/devtools/overlay/controller_spec.rb +58 -0
- data/gems/e11y-devtools/spec/e11y/devtools/overlay/middleware_spec.rb +46 -0
- data/gems/e11y-devtools/spec/e11y/devtools/tui/app_spec.rb +85 -0
- data/gems/e11y-devtools/spec/e11y/devtools/tui/grouping_spec.rb +64 -0
- data/gems/e11y-devtools/spec/spec_helper.rb +5 -0
- data/gems/e11y-devtools/spec/tui/widgets/event_list_spec.rb +44 -0
- data/gems/e11y-devtools/spec/tui/widgets/interaction_list_spec.rb +62 -0
- data/lib/e11y/adapters/audit_encrypted.rb +53 -11
- data/lib/e11y/adapters/base.rb +33 -34
- data/lib/e11y/adapters/dev_log/file_store.rb +143 -0
- data/lib/e11y/adapters/dev_log/query.rb +219 -0
- data/lib/e11y/adapters/dev_log.rb +118 -0
- data/lib/e11y/adapters/file.rb +3 -6
- data/lib/e11y/adapters/in_memory.rb +52 -5
- data/lib/e11y/adapters/in_memory_test.rb +29 -0
- data/lib/e11y/adapters/loki.rb +58 -23
- data/lib/e11y/adapters/null.rb +82 -0
- data/lib/e11y/adapters/opentelemetry_collector.rb +183 -0
- data/lib/e11y/adapters/otel_logs.rb +136 -23
- data/lib/e11y/adapters/sentry.rb +4 -7
- data/lib/e11y/adapters/stdout.rb +73 -7
- data/lib/e11y/adapters/yabeda.rb +153 -29
- data/lib/e11y/buffers/adaptive_buffer.rb +3 -17
- data/lib/e11y/buffers/{request_scoped_buffer.rb → ephemeral_buffer.rb} +72 -58
- data/lib/e11y/buffers/ring_buffer.rb +3 -16
- data/lib/e11y/configuration.rb +272 -0
- data/lib/e11y/console.rb +10 -17
- data/lib/e11y/current.rb +53 -1
- data/lib/e11y/debug/pipeline_inspector.rb +96 -0
- data/lib/e11y/documentation/generator.rb +48 -0
- data/lib/e11y/event/base.rb +176 -82
- data/lib/e11y/event/value_sampling_config.rb +1 -5
- data/lib/e11y/events/rails/database/query.rb +1 -4
- data/lib/e11y/events/rails/job/failed.rb +2 -0
- data/lib/e11y/instruments/active_job.rb +46 -12
- data/lib/e11y/instruments/rails_instrumentation.rb +49 -24
- data/lib/e11y/instruments/sidekiq.rb +137 -31
- data/lib/e11y/linters/base.rb +11 -0
- data/lib/e11y/linters/pii/pii_declaration_linter.rb +120 -0
- data/lib/e11y/linters/slo/config_consistency_linter.rb +76 -0
- data/lib/e11y/linters/slo/explicit_declaration_linter.rb +36 -0
- data/lib/e11y/linters/slo/slo_status_from_linter.rb +41 -0
- data/lib/e11y/logger/bridge.rb +26 -7
- data/lib/e11y/metrics/cardinality_protection.rb +10 -15
- data/lib/e11y/metrics/cardinality_tracker.rb +16 -6
- data/lib/e11y/metrics/registry.rb +3 -5
- data/lib/e11y/metrics/test_backend.rb +62 -0
- data/lib/e11y/metrics.rb +56 -10
- data/lib/e11y/middleware/adapter_resolver.rb +40 -0
- data/lib/e11y/middleware/audit_signing.rb +43 -6
- data/lib/e11y/middleware/baggage_protection.rb +75 -0
- data/lib/e11y/middleware/dev_log_source.rb +24 -0
- data/lib/e11y/middleware/event_slo.rb +23 -9
- data/lib/e11y/middleware/otel_span.rb +23 -0
- data/lib/e11y/middleware/pii_filter.rb +104 -75
- data/lib/e11y/middleware/rate_limiting.rb +54 -27
- data/lib/e11y/middleware/request.rb +70 -23
- data/lib/e11y/middleware/routing.rb +78 -21
- data/lib/e11y/middleware/sampling.rb +66 -17
- data/lib/e11y/middleware/self_monitoring_emit.rb +39 -0
- data/lib/e11y/middleware/trace_context.rb +45 -10
- data/lib/e11y/middleware/track_latency.rb +34 -0
- data/lib/e11y/middleware/validation.rb +7 -16
- data/lib/e11y/middleware/versioning.rb +26 -22
- data/lib/e11y/opentelemetry/semantic_conventions.rb +109 -0
- data/lib/e11y/opentelemetry/span_creator.rb +142 -0
- data/lib/e11y/pii/patterns.rb +12 -1
- data/lib/e11y/pipeline/builder.rb +1 -1
- data/lib/e11y/presets/audit_event.rb +13 -2
- data/lib/e11y/railtie.rb +52 -15
- data/lib/e11y/registry.rb +306 -0
- data/lib/e11y/reliability/circuit_breaker.rb +19 -21
- data/lib/e11y/reliability/dlq/base.rb +71 -0
- data/lib/e11y/reliability/dlq/file_adapter.rb +301 -0
- data/lib/e11y/reliability/dlq/file_storage.rb +63 -34
- data/lib/e11y/reliability/dlq/filter.rb +37 -54
- data/lib/e11y/reliability/retry_handler.rb +26 -29
- data/lib/e11y/reliability/retry_rate_limiter.rb +3 -11
- data/lib/e11y/sampling/error_spike_detector.rb +0 -2
- data/lib/e11y/sampling/load_monitor.rb +5 -9
- data/lib/e11y/sampling/stratified_tracker.rb +18 -0
- data/lib/e11y/self_monitoring/buffer_monitor.rb +2 -0
- data/lib/e11y/self_monitoring/performance_monitor.rb +19 -61
- data/lib/e11y/self_monitoring/reliability_monitor.rb +4 -74
- data/lib/e11y/slo/config_loader.rb +40 -0
- data/lib/e11y/slo/config_validator.rb +58 -0
- data/lib/e11y/slo/dashboard_generator.rb +122 -0
- data/lib/e11y/slo/event_driven.rb +8 -0
- data/lib/e11y/slo/tracker.rb +31 -4
- data/lib/e11y/testing/have_tracked_event_matcher.rb +190 -0
- data/lib/e11y/testing/rspec_matchers.rb +21 -0
- data/lib/e11y/testing/snapshot_matcher.rb +86 -0
- data/lib/e11y/trace_context/sampler.rb +35 -0
- data/lib/e11y/tracing/faraday_middleware.rb +31 -0
- data/lib/e11y/tracing/net_http_patch.rb +33 -0
- data/lib/e11y/tracing/propagator.rb +116 -0
- data/lib/e11y/tracing.rb +47 -0
- data/lib/e11y/version.rb +1 -1
- data/lib/e11y/versioning/version_extractor.rb +32 -0
- data/lib/e11y.rb +141 -265
- data/lib/generators/e11y/event/event_generator.rb +22 -0
- data/lib/generators/e11y/event/templates/event.rb.tt +16 -0
- data/lib/generators/e11y/grafana_dashboard/grafana_dashboard_generator.rb +30 -0
- data/lib/generators/e11y/grafana_dashboard/templates/e11y_dashboard.json +81 -0
- data/lib/generators/e11y/install/install_generator.rb +34 -0
- data/lib/generators/e11y/install/templates/e11y.rb +239 -0
- data/lib/generators/e11y/prometheus_alerts/prometheus_alerts_generator.rb +29 -0
- data/lib/generators/e11y/prometheus_alerts/templates/e11y_alerts.yml +28 -0
- data/lib/tasks/e11y_docs.rake +30 -0
- data/lib/tasks/e11y_events.rake +71 -0
- data/lib/tasks/e11y_lint.rake +91 -0
- data/lib/tasks/e11y_slo.rake +29 -0
- metadata +129 -39
- data/docs/ADR-010-developer-experience.md +0 -2166
- data/docs/API-REFERENCE-L28.md +0 -914
- data/docs/COMPREHENSIVE-CONFIGURATION.md +0 -2366
- data/docs/CONTRIBUTING.md +0 -312
- data/docs/IMPLEMENTATION_NOTES.md +0 -2804
- data/docs/IMPLEMENTATION_PLAN.md +0 -1971
- data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +0 -586
- data/docs/PLAN.md +0 -148
- data/docs/README.md +0 -296
- data/docs/design/00-memory-optimization.md +0 -593
- data/docs/guides/MIGRATION-L27-L28.md +0 -692
- data/docs/guides/PERFORMANCE-BENCHMARKS.md +0 -434
- data/docs/guides/README.md +0 -44
- data/docs/use_cases/UC-003-pattern-based-metrics.md +0 -1627
- data/lib/e11y/adapters/registry.rb +0 -141
- /data/docs/{ADR-012-event-evolution.md → architecture/ADR-012-event-evolution.md} +0 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "rack/test"
|
|
5
|
+
require "rack/mock"
|
|
6
|
+
require "e11y/devtools/overlay/middleware"
|
|
7
|
+
|
|
8
|
+
RSpec.describe E11y::Devtools::Overlay::Middleware do
|
|
9
|
+
include Rack::Test::Methods
|
|
10
|
+
|
|
11
|
+
let(:html_body) { "<html><body><h1>Hello</h1></body></html>" }
|
|
12
|
+
let(:base_app) do
|
|
13
|
+
->(_env) { [200, { "Content-Type" => "text/html" }, [html_body]] }
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def app = described_class.new(base_app)
|
|
17
|
+
|
|
18
|
+
it "injects overlay script before </body>" do
|
|
19
|
+
get "/"
|
|
20
|
+
expect(last_response.body).to include("e11y-overlay")
|
|
21
|
+
expect(last_response.body).to include("</body>")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
it "does not inject into non-HTML responses" do
|
|
25
|
+
json_app = ->(_env) { [200, { "Content-Type" => "application/json" }, ['{"ok":true}']] }
|
|
26
|
+
response = described_class.new(json_app).call(Rack::MockRequest.env_for("/"))
|
|
27
|
+
body = response[2].join
|
|
28
|
+
expect(body).not_to include("e11y-overlay")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
it "does not inject into XHR requests" do
|
|
32
|
+
get "/", {}, { "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest" }
|
|
33
|
+
expect(last_response.body).not_to include("e11y-overlay")
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "does not inject into asset paths" do
|
|
37
|
+
get "/assets/application.js"
|
|
38
|
+
expect(last_response.body).not_to include("e11y-overlay")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "preserves Content-Length consistency after injection" do
|
|
42
|
+
get "/"
|
|
43
|
+
content_length = last_response.headers["Content-Length"]
|
|
44
|
+
expect(content_length.to_i).to eq(last_response.body.bytesize) if content_length
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "e11y/adapters/dev_log/query"
|
|
6
|
+
require "e11y/devtools/tui/grouping"
|
|
7
|
+
require "e11y/devtools/tui/app"
|
|
8
|
+
|
|
9
|
+
RSpec.describe E11y::Devtools::Tui::App do
|
|
10
|
+
subject(:app) { described_class.new(log_path: "/dev/null") }
|
|
11
|
+
|
|
12
|
+
describe "#initialize" do
|
|
13
|
+
it "starts in :interactions view" do
|
|
14
|
+
expect(app.current_view).to eq(:interactions)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "starts with source_filter :web" do
|
|
18
|
+
expect(app.source_filter).to eq(:web)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
describe "#handle_key" do
|
|
23
|
+
context "when in :interactions view" do
|
|
24
|
+
it "drills into :events on Enter" do
|
|
25
|
+
query = instance_double(E11y::Adapters::DevLog::Query, events_by_trace: [])
|
|
26
|
+
interaction = E11y::Devtools::Tui::Grouping::Interaction.new(
|
|
27
|
+
Time.now, ["t1"], false, "web"
|
|
28
|
+
)
|
|
29
|
+
app.instance_variable_set(:@query, query)
|
|
30
|
+
app.instance_variable_set(:@interactions, [interaction])
|
|
31
|
+
app.instance_variable_set(:@selected_ix, 0)
|
|
32
|
+
app.handle_key("enter")
|
|
33
|
+
expect(app.current_view).to eq(:events)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "toggles source to :job on 'j'" do
|
|
37
|
+
app.handle_key("j")
|
|
38
|
+
expect(app.source_filter).to eq(:job)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "toggles source to :all on 'a'" do
|
|
42
|
+
app.handle_key("a")
|
|
43
|
+
expect(app.source_filter).to eq(:all)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
it "toggles source back to :web on 'w'" do
|
|
47
|
+
app.handle_key("a")
|
|
48
|
+
app.handle_key("w")
|
|
49
|
+
expect(app.source_filter).to eq(:web)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
context "when in :events view" do
|
|
54
|
+
before do
|
|
55
|
+
app.instance_variable_set(:@current_view, :events)
|
|
56
|
+
app.instance_variable_set(:@current_trace_id, "t1")
|
|
57
|
+
app.instance_variable_set(:@events, [{ "id" => "e1", "event_name" => "x" }])
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
it "goes back to :interactions on Esc" do
|
|
61
|
+
app.handle_key("esc")
|
|
62
|
+
expect(app.current_view).to eq(:interactions)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "drills into :detail on Enter" do
|
|
66
|
+
app.handle_key("enter")
|
|
67
|
+
expect(app.current_view).to eq(:detail)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context "when in :detail view" do
|
|
72
|
+
before { app.instance_variable_set(:@current_view, :detail) }
|
|
73
|
+
|
|
74
|
+
it "goes back to :events on Esc" do
|
|
75
|
+
app.handle_key("esc")
|
|
76
|
+
expect(app.current_view).to eq(:events)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "goes back to :events on 'b'" do
|
|
80
|
+
app.handle_key("b")
|
|
81
|
+
expect(app.current_view).to eq(:events)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "time"
|
|
5
|
+
require "e11y/devtools/tui/grouping"
|
|
6
|
+
|
|
7
|
+
RSpec.describe E11y::Devtools::Tui::Grouping do
|
|
8
|
+
def make_trace(id, offset_ms, severity: "info", source: "web")
|
|
9
|
+
{
|
|
10
|
+
trace_id: id,
|
|
11
|
+
started_at: Time.now + (offset_ms / 1000.0),
|
|
12
|
+
severity: severity,
|
|
13
|
+
source: source
|
|
14
|
+
}
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
describe ".group" do
|
|
18
|
+
it "returns empty array for empty input" do
|
|
19
|
+
expect(described_class.group([])).to eq([])
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it "places single trace in one interaction" do
|
|
23
|
+
groups = described_class.group([make_trace("t1", 0)])
|
|
24
|
+
expect(groups.size).to eq(1)
|
|
25
|
+
expect(groups.first.trace_ids).to eq(["t1"])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
it "groups traces within window into one interaction" do
|
|
29
|
+
traces = [
|
|
30
|
+
make_trace("t1", 0),
|
|
31
|
+
make_trace("t2", 300), # 300ms after t1 — within 500ms window
|
|
32
|
+
make_trace("t3", 1200) # 1200ms after t1 — outside window
|
|
33
|
+
]
|
|
34
|
+
groups = described_class.group(traces, window_ms: 500)
|
|
35
|
+
expect(groups.size).to eq(2)
|
|
36
|
+
# newest-first: groups[0] = t3 group, groups[1] = t1+t2 group
|
|
37
|
+
expect(groups.last.trace_ids.sort).to eq(%w[t1 t2].sort)
|
|
38
|
+
expect(groups.first.trace_ids).to eq(["t3"])
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it "marks interaction has_error? when any trace has error severity" do
|
|
42
|
+
traces = [
|
|
43
|
+
make_trace("t1", 0, severity: "error"),
|
|
44
|
+
make_trace("t2", 100, severity: "info")
|
|
45
|
+
]
|
|
46
|
+
groups = described_class.group(traces, window_ms: 500)
|
|
47
|
+
expect(groups.first.has_error?).to be true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "marks interaction clean when no errors" do
|
|
51
|
+
groups = described_class.group([make_trace("t1", 0, severity: "info")])
|
|
52
|
+
expect(groups.first.has_error?).to be false
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it "returns groups newest-first" do
|
|
56
|
+
traces = [
|
|
57
|
+
make_trace("old", 0),
|
|
58
|
+
make_trace("new", 2000)
|
|
59
|
+
]
|
|
60
|
+
groups = described_class.group(traces, window_ms: 500)
|
|
61
|
+
expect(groups.first.trace_ids).to eq(["new"])
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "time"
|
|
5
|
+
|
|
6
|
+
unless defined?(RATATUI_AVAILABLE)
|
|
7
|
+
begin
|
|
8
|
+
require "minitest"
|
|
9
|
+
require "ratatui_ruby"
|
|
10
|
+
require "ratatui_ruby/test_helper"
|
|
11
|
+
RATATUI_AVAILABLE = true
|
|
12
|
+
rescue LoadError
|
|
13
|
+
RATATUI_AVAILABLE = false
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
RSpec.describe "E11y::Devtools::Tui::Widgets::EventList" do
|
|
18
|
+
include RatatuiRuby::TestHelper if RATATUI_AVAILABLE
|
|
19
|
+
|
|
20
|
+
before do
|
|
21
|
+
skip "ratatui_ruby not available" unless RATATUI_AVAILABLE
|
|
22
|
+
require "e11y/devtools/tui/widgets/event_list"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
let(:events) do
|
|
26
|
+
[
|
|
27
|
+
{ "severity" => "error", "event_name" => "order.failed",
|
|
28
|
+
"timestamp" => Time.now.iso8601, "metadata" => {} },
|
|
29
|
+
{ "severity" => "info", "event_name" => "order.created",
|
|
30
|
+
"timestamp" => Time.now.iso8601, "metadata" => { "duration_ms" => 42 } }
|
|
31
|
+
]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
it "renders without raising" do
|
|
35
|
+
widget = E11y::Devtools::Tui::Widgets::EventList.new(
|
|
36
|
+
events: events, trace_id: "trace-1", selected_index: 0
|
|
37
|
+
)
|
|
38
|
+
tui = RatatuiRuby::TUI.new
|
|
39
|
+
with_test_terminal(80, 10) do
|
|
40
|
+
expect { RatatuiRuby.draw { |frame| widget.render(tui, frame, frame.area) } }
|
|
41
|
+
.not_to raise_error
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "time"
|
|
5
|
+
require "e11y/devtools/tui/grouping"
|
|
6
|
+
|
|
7
|
+
unless defined?(RATATUI_AVAILABLE)
|
|
8
|
+
begin
|
|
9
|
+
require "minitest"
|
|
10
|
+
require "ratatui_ruby"
|
|
11
|
+
require "ratatui_ruby/test_helper"
|
|
12
|
+
RATATUI_AVAILABLE = true
|
|
13
|
+
rescue LoadError
|
|
14
|
+
RATATUI_AVAILABLE = false
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
RSpec.describe "E11y::Devtools::Tui::Widgets::InteractionList" do
|
|
19
|
+
include RatatuiRuby::TestHelper if RATATUI_AVAILABLE
|
|
20
|
+
|
|
21
|
+
before do
|
|
22
|
+
skip "ratatui_ruby not available" unless RATATUI_AVAILABLE
|
|
23
|
+
require "e11y/devtools/tui/widgets/interaction_list"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
let(:t0) { Time.now }
|
|
27
|
+
|
|
28
|
+
def make_interaction(trace_ids:, has_error: false)
|
|
29
|
+
E11y::Devtools::Tui::Grouping::Interaction.new(
|
|
30
|
+
started_at: t0,
|
|
31
|
+
trace_ids: trace_ids,
|
|
32
|
+
has_error?: has_error,
|
|
33
|
+
source: "web"
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "renders bullet as ● red when interaction has error" do
|
|
38
|
+
widget = E11y::Devtools::Tui::Widgets::InteractionList.new(
|
|
39
|
+
interactions: [make_interaction(trace_ids: ["t1"], has_error: true)],
|
|
40
|
+
selected_index: 0
|
|
41
|
+
)
|
|
42
|
+
tui = RatatuiRuby::TUI.new
|
|
43
|
+
with_test_terminal(40, 5) do
|
|
44
|
+
expect { RatatuiRuby.draw { |frame| widget.render(tui, frame, frame.area) } }
|
|
45
|
+
.not_to raise_error
|
|
46
|
+
expect(buffer_content.join).to include("●")
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
it "renders bullet as ○ when interaction is clean" do
|
|
51
|
+
widget = E11y::Devtools::Tui::Widgets::InteractionList.new(
|
|
52
|
+
interactions: [make_interaction(trace_ids: ["t1"], has_error: false)],
|
|
53
|
+
selected_index: 0
|
|
54
|
+
)
|
|
55
|
+
tui = RatatuiRuby::TUI.new
|
|
56
|
+
with_test_terminal(40, 5) do
|
|
57
|
+
expect { RatatuiRuby.draw { |frame| widget.render(tui, frame, frame.area) } }
|
|
58
|
+
.not_to raise_error
|
|
59
|
+
expect(buffer_content.join).to include("○")
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -4,6 +4,7 @@ require "openssl"
|
|
|
4
4
|
require "json"
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require "base64"
|
|
7
|
+
require "e11y/event/base"
|
|
7
8
|
|
|
8
9
|
module E11y
|
|
9
10
|
module Adapters
|
|
@@ -84,14 +85,47 @@ module E11y
|
|
|
84
85
|
# Read and decrypt audit event (for verification)
|
|
85
86
|
#
|
|
86
87
|
# @param event_id [String] Event ID
|
|
87
|
-
# @return [Hash] Decrypted event data
|
|
88
|
+
# @return [Hash, nil] Decrypted event data, or nil if decryption fails
|
|
88
89
|
def read(event_id)
|
|
89
90
|
encrypted_data = read_from_storage(event_id)
|
|
90
91
|
decrypt_event(encrypted_data)
|
|
92
|
+
rescue Errno::ENOENT => e
|
|
93
|
+
warn "AuditEncrypted read error (file not found): #{e.message}"
|
|
94
|
+
nil
|
|
95
|
+
rescue JSON::ParserError => e
|
|
96
|
+
warn "AuditEncrypted read error (corrupt data): #{e.message}"
|
|
97
|
+
nil
|
|
98
|
+
rescue OpenSSL::Cipher::CipherError => e
|
|
99
|
+
# SECURITY: decryption failure indicates tampered or corrupt ciphertext.
|
|
100
|
+
# Re-raise so callers can handle it; also attempt to emit a security event.
|
|
101
|
+
track_security_event(event_id, e)
|
|
102
|
+
raise
|
|
91
103
|
end
|
|
92
104
|
|
|
93
105
|
private
|
|
94
106
|
|
|
107
|
+
# Emit a security event when decryption fails (potential tampering).
|
|
108
|
+
# Guards against E11y not being fully configured in non-production envs.
|
|
109
|
+
#
|
|
110
|
+
# @param event_id [String] The event ID that failed to decrypt
|
|
111
|
+
# @param error [OpenSSL::Cipher::CipherError] The decryption error
|
|
112
|
+
# @return [void]
|
|
113
|
+
def track_security_event(event_id, error)
|
|
114
|
+
E11y::Event::Base.track(
|
|
115
|
+
event_name: "e11y.security.audit_decryption_failed",
|
|
116
|
+
severity: :error,
|
|
117
|
+
payload: {
|
|
118
|
+
event_id: event_id,
|
|
119
|
+
error_class: error.class.name,
|
|
120
|
+
error_message: error.message,
|
|
121
|
+
adapter: self.class.name
|
|
122
|
+
}
|
|
123
|
+
)
|
|
124
|
+
rescue StandardError
|
|
125
|
+
warn "AuditEncrypted: decryption failure detected for #{event_id} " \
|
|
126
|
+
"(#{error.message}); security event could not be tracked"
|
|
127
|
+
end
|
|
128
|
+
|
|
95
129
|
# Encrypt event data with AES-256-GCM
|
|
96
130
|
#
|
|
97
131
|
# @param event_data [Hash] Event data
|
|
@@ -127,7 +161,6 @@ module E11y
|
|
|
127
161
|
#
|
|
128
162
|
# @param encrypted [Hash] Encrypted data with nonce and tag
|
|
129
163
|
# @return [Hash] Decrypted event data
|
|
130
|
-
# rubocop:disable Metrics/AbcSize
|
|
131
164
|
# Cryptographic operations require multiple steps for secure decryption
|
|
132
165
|
def decrypt_event(encrypted)
|
|
133
166
|
cipher = OpenSSL::Cipher.new(CIPHER)
|
|
@@ -141,7 +174,6 @@ module E11y
|
|
|
141
174
|
|
|
142
175
|
JSON.parse(plaintext, symbolize_names: true)
|
|
143
176
|
end
|
|
144
|
-
# rubocop:enable Metrics/AbcSize
|
|
145
177
|
|
|
146
178
|
# Write encrypted data to storage
|
|
147
179
|
#
|
|
@@ -216,17 +248,27 @@ module E11y
|
|
|
216
248
|
#
|
|
217
249
|
# @return [String] Encryption key
|
|
218
250
|
def default_encryption_key
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
251
|
+
# Use ENV var if provided (required in production)
|
|
252
|
+
env_key = ENV.fetch("E11Y_AUDIT_ENCRYPTION_KEY", nil)
|
|
253
|
+
if env_key
|
|
254
|
+
return env_key.bytesize == 32 ? env_key : [env_key].pack("H*")
|
|
255
|
+
end
|
|
223
256
|
|
|
224
|
-
|
|
225
|
-
|
|
257
|
+
# In production without ENV var, raise a clear error
|
|
258
|
+
if defined?(::Rails) && ::Rails.env.production?
|
|
259
|
+
raise E11y::Error,
|
|
260
|
+
"E11Y_AUDIT_ENCRYPTION_KEY must be set in production. " \
|
|
261
|
+
"Generate with: openssl rand -hex 32"
|
|
226
262
|
end
|
|
227
263
|
|
|
228
|
-
#
|
|
229
|
-
|
|
264
|
+
# Development/test: derive a stable key from a fixed seed.
|
|
265
|
+
# This is NOT secure for production — only for development/testing.
|
|
266
|
+
OpenSSL::PKCS5.pbkdf2_hmac_sha1(
|
|
267
|
+
"e11y-development-key-not-for-production",
|
|
268
|
+
"e11y-static-salt",
|
|
269
|
+
1000,
|
|
270
|
+
32
|
|
271
|
+
)
|
|
230
272
|
end
|
|
231
273
|
|
|
232
274
|
# Default storage path
|
data/lib/e11y/adapters/base.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative "../reliability/retry_handler"
|
|
4
|
+
require_relative "../reliability/retry_rate_limiter"
|
|
4
5
|
require_relative "../reliability/circuit_breaker"
|
|
5
6
|
|
|
6
7
|
module E11y
|
|
@@ -98,14 +99,13 @@ module E11y
|
|
|
98
99
|
# This is the recommended public API for sending events.
|
|
99
100
|
# Automatically handles failures, retries, and DLQ.
|
|
100
101
|
#
|
|
101
|
-
# Respects `E11y.config.
|
|
102
|
+
# Respects `E11y.config.error_handling_fail_on_error` setting (C18 Resolution):
|
|
102
103
|
# - `true`: Raises exceptions (fast feedback for web requests)
|
|
103
104
|
# - `false`: Swallows exceptions, saves to DLQ (don't fail background jobs)
|
|
104
105
|
#
|
|
105
106
|
# @param event_data [Hash] Event payload
|
|
106
107
|
# @return [Boolean] true on success
|
|
107
108
|
# @raise [RetryExhaustedError, CircuitOpenError] if fail_on_error=true
|
|
108
|
-
# rubocop:disable Metrics/MethodLength
|
|
109
109
|
# Core reliability logic with retry and circuit breaker - should stay as cohesive unit
|
|
110
110
|
def write_with_reliability(event_data)
|
|
111
111
|
return write(event_data) unless @reliability_enabled
|
|
@@ -129,7 +129,6 @@ module E11y
|
|
|
129
129
|
handle_reliability_error(event_data, e, :circuit_open)
|
|
130
130
|
end
|
|
131
131
|
end
|
|
132
|
-
# rubocop:enable Metrics/MethodLength
|
|
133
132
|
|
|
134
133
|
# Write a batch of events (preferred for performance)
|
|
135
134
|
#
|
|
@@ -288,7 +287,7 @@ module E11y
|
|
|
288
287
|
raise unless retriable_error?(e) && attempt < max_attempts
|
|
289
288
|
|
|
290
289
|
delay = calculate_backoff_delay(attempt, base_delay, max_delay, jitter)
|
|
291
|
-
warn
|
|
290
|
+
E11y.logger&.warn("[E11y] #{self.class.name} retry #{attempt}/#{max_attempts} after #{delay.round(2)}s: #{e.message}")
|
|
292
291
|
sleep(delay)
|
|
293
292
|
retry
|
|
294
293
|
end
|
|
@@ -306,7 +305,7 @@ module E11y
|
|
|
306
305
|
# def retriable_error?(error)
|
|
307
306
|
# super || error.is_a?(CustomTransientError)
|
|
308
307
|
# end
|
|
309
|
-
# rubocop:disable Metrics/
|
|
308
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
310
309
|
# This method checks many different error types for retryability - splitting would reduce clarity
|
|
311
310
|
def retriable_error?(error)
|
|
312
311
|
# Network timeout errors
|
|
@@ -334,7 +333,7 @@ module E11y
|
|
|
334
333
|
|
|
335
334
|
false
|
|
336
335
|
end
|
|
337
|
-
# rubocop:enable Metrics/
|
|
336
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
338
337
|
|
|
339
338
|
# Calculate exponential backoff delay with jitter
|
|
340
339
|
#
|
|
@@ -381,16 +380,13 @@ module E11y
|
|
|
381
380
|
# end
|
|
382
381
|
#
|
|
383
382
|
# @see ADR-004 Section 7.2 (Circuit Breaker)
|
|
384
|
-
# rubocop:disable Metrics/MethodLength
|
|
385
383
|
# Circuit breaker state machine logic should stay as cohesive unit
|
|
386
384
|
def with_circuit_breaker(failure_threshold: 5, timeout: 60)
|
|
387
385
|
init_circuit_breaker! unless @circuit_state
|
|
388
386
|
|
|
389
387
|
@circuit_mutex.synchronize do
|
|
390
388
|
if @circuit_state == :open
|
|
391
|
-
unless circuit_timeout_expired?(timeout)
|
|
392
|
-
raise CircuitOpenError, "Circuit breaker open for #{self.class.name}"
|
|
393
|
-
end
|
|
389
|
+
raise CircuitOpenError, "Circuit breaker open for #{self.class.name}" unless circuit_timeout_expired?(timeout)
|
|
394
390
|
|
|
395
391
|
@circuit_state = :half_open
|
|
396
392
|
@circuit_success_count = 0
|
|
@@ -407,7 +403,6 @@ module E11y
|
|
|
407
403
|
raise
|
|
408
404
|
end
|
|
409
405
|
end
|
|
410
|
-
# rubocop:enable Metrics/MethodLength
|
|
411
406
|
|
|
412
407
|
# Initialize circuit breaker state
|
|
413
408
|
#
|
|
@@ -431,7 +426,7 @@ module E11y
|
|
|
431
426
|
@circuit_success_count += 1
|
|
432
427
|
if @circuit_success_count >= 2 # 2 successes → close
|
|
433
428
|
@circuit_state = :closed
|
|
434
|
-
warn
|
|
429
|
+
E11y.logger&.warn("[E11y] #{self.class.name} circuit breaker closed (recovered)")
|
|
435
430
|
end
|
|
436
431
|
end
|
|
437
432
|
end
|
|
@@ -449,7 +444,7 @@ module E11y
|
|
|
449
444
|
|
|
450
445
|
if @circuit_failure_count >= threshold && @circuit_state == :closed
|
|
451
446
|
@circuit_state = :open
|
|
452
|
-
warn
|
|
447
|
+
E11y.logger&.warn("[E11y] #{self.class.name} circuit breaker opened (#{@circuit_failure_count} failures)")
|
|
453
448
|
end
|
|
454
449
|
end
|
|
455
450
|
end
|
|
@@ -469,9 +464,14 @@ module E11y
|
|
|
469
464
|
def setup_reliability_layer
|
|
470
465
|
reliability_config = @config.fetch(:reliability, {})
|
|
471
466
|
|
|
472
|
-
# Setup RetryHandler
|
|
467
|
+
# Setup RetryHandler (C06: wire RetryRateLimiter for thundering herd prevention)
|
|
473
468
|
retry_config = reliability_config.fetch(:retry, {})
|
|
474
|
-
|
|
469
|
+
rate_limiter = reliability_config[:retry_rate_limiter] ||
|
|
470
|
+
E11y::Reliability::RetryRateLimiter.new
|
|
471
|
+
@retry_handler = E11y::Reliability::RetryHandler.new(
|
|
472
|
+
config: retry_config,
|
|
473
|
+
rate_limiter: rate_limiter
|
|
474
|
+
)
|
|
475
475
|
|
|
476
476
|
# Setup CircuitBreaker
|
|
477
477
|
circuit_breaker_config = reliability_config.fetch(:circuit_breaker, {})
|
|
@@ -479,15 +479,11 @@ module E11y
|
|
|
479
479
|
adapter_name: self.class.name,
|
|
480
480
|
config: circuit_breaker_config
|
|
481
481
|
)
|
|
482
|
-
|
|
483
|
-
# Setup DLQ components (will be initialized from E11y.config later)
|
|
484
|
-
@dlq_filter = nil
|
|
485
|
-
@dlq_storage = nil
|
|
486
482
|
end
|
|
487
483
|
|
|
488
484
|
# Handle reliability error (retry exhausted / circuit breaker open).
|
|
489
485
|
#
|
|
490
|
-
# Behavior depends on `E11y.config.
|
|
486
|
+
# Behavior depends on `E11y.config.error_handling_fail_on_error` (C18 Resolution):
|
|
491
487
|
# - `true`: Re-raises exception (fast feedback for web requests)
|
|
492
488
|
# - `false`: Swallows exception, saves to DLQ (don't fail background jobs)
|
|
493
489
|
#
|
|
@@ -505,35 +501,38 @@ module E11y
|
|
|
505
501
|
save_to_dlq_if_needed(event_data, error, reason)
|
|
506
502
|
|
|
507
503
|
# Log warning
|
|
508
|
-
warn
|
|
504
|
+
E11y.logger&.warn("[E11y] #{self.class.name} #{reason} for event #{event_data[:event_name]}: #{error.message}")
|
|
509
505
|
|
|
510
506
|
# Check fail_on_error setting (C18 Resolution)
|
|
511
|
-
raise error if E11y.config.
|
|
507
|
+
raise error if E11y.config.error_handling_fail_on_error
|
|
512
508
|
|
|
513
509
|
# Web request context: RAISE (fast feedback)
|
|
514
510
|
|
|
515
511
|
# Background job context: SWALLOW (don't fail business logic)
|
|
516
|
-
# TODO: Track metric e11y.event.tracking_failed_silent
|
|
517
512
|
false
|
|
518
513
|
end
|
|
519
514
|
# rubocop:enable Naming/PredicateMethod
|
|
520
515
|
|
|
521
516
|
# Save event to DLQ if filter allows.
|
|
517
|
+
# Uses E11y.config.dlq_filter and E11y.config.dlq_storage (F3 — wired from config).
|
|
522
518
|
#
|
|
523
519
|
# @api private
|
|
524
520
|
def save_to_dlq_if_needed(event_data, error, reason)
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
521
|
+
dlq_filter = E11y.config.respond_to?(:dlq_filter) ? E11y.config.dlq_filter : nil
|
|
522
|
+
dlq_storage = E11y.config.respond_to?(:dlq_storage) ? E11y.config.dlq_storage : nil
|
|
523
|
+
return unless dlq_filter&.should_save?(event_data, error)
|
|
524
|
+
return unless dlq_storage
|
|
525
|
+
|
|
526
|
+
dlq_storage.save(event_data, metadata: {
|
|
527
|
+
error: error,
|
|
528
|
+
error_class: error.class.name,
|
|
529
|
+
reason: reason,
|
|
530
|
+
adapter: self.class.name,
|
|
531
|
+
timestamp: Time.now.utc.iso8601
|
|
532
|
+
})
|
|
534
533
|
rescue StandardError => e
|
|
535
534
|
# C18: Don't fail if DLQ save fails
|
|
536
|
-
warn
|
|
535
|
+
E11y.logger&.warn("[E11y] Failed to save event to DLQ: #{e.message}")
|
|
537
536
|
end
|
|
538
537
|
|
|
539
538
|
# Track successful adapter write (self-monitoring).
|
|
@@ -558,7 +557,7 @@ module E11y
|
|
|
558
557
|
)
|
|
559
558
|
rescue StandardError => e
|
|
560
559
|
# Don't fail if monitoring fails
|
|
561
|
-
warn
|
|
560
|
+
E11y.logger&.warn("[E11y] Self-monitoring error: #{e.message}")
|
|
562
561
|
end
|
|
563
562
|
|
|
564
563
|
# Track failed adapter write (self-monitoring).
|