e11y 0.2.0 → 1.1.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 +80 -1
- data/CLAUDE.md +168 -0
- data/CONTRIBUTING.md +640 -0
- data/README.md +165 -701
- data/RELEASE.md +41 -12
- data/Rakefile +249 -57
- 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 +79 -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} +36 -65
- data/docs/{ADR-002-metrics-yabeda.md → architecture/ADR-002-metrics-yabeda.md} +62 -236
- data/docs/architecture/ADR-003-slo-observability.md +1402 -0
- 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} +182 -743
- 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} +44 -86
- data/docs/{ADR-012-event-evolution.md → architecture/ADR-012-event-evolution.md} +11 -11
- 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} +43 -59
- data/docs/{ADR-016-self-monitoring-slo.md → architecture/ADR-016-self-monitoring-slo.md} +58 -355
- 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/plans/2026-03-20-browser-overlay-svelte.md +281 -0
- 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 +33 -684
- 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 +30 -178
- data/docs/use_cases/UC-010-background-job-tracking.md +24 -91
- 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 +158 -0
- data/gems/e11y-devtools/config/routes.rb +15 -0
- data/gems/e11y-devtools/e11y-devtools.gemspec +25 -0
- data/gems/e11y-devtools/exe/e11y +34 -0
- data/gems/e11y-devtools/frontend/.gitignore +24 -0
- data/gems/e11y-devtools/frontend/README.md +51 -0
- data/gems/e11y-devtools/frontend/index.html +14 -0
- data/gems/e11y-devtools/frontend/package-lock.json +3707 -0
- data/gems/e11y-devtools/frontend/package.json +28 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/events/recent.json +4205 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/interactions.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/0a2e04027cfa22d014bc22e8b27cd913/events.json +86 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/0e1543af6a630fb3af6b52283154b3e0/events.json +169 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/1838b691faa49564f97db8592ff3978d/events.json +78 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/29f198f6588dacffb687777eb5f8f118/events.json +197 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/34bc3c9c0097de28a7a6f99b90a8e7bc/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/3ba6c20d068ab9cee00e51b180e66444/events.json +184 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/435bfd8f17b9009146a79812d7c3726d/events.json +144 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/4c7676e3fe668e99edb2b94d7d5678a9/events.json +222 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/6daf0d47974bedfc55d5de7004a3ea9f/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8a81ada42834d15f287bb40010043605/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8c0a98900edaae105469df8daedccf02/events.json +198 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8e4f645180f8a7d1dce426b07380466b/events.json +222 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/93db346fa5d44a032605a13b627f4b80/events.json +128 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/98ff6146faf7bd9be8bd03a8275817ba/events.json +223 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/9997ddd0247bc7e25f2ca7a5c415c93d/events.json +197 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/99e35f8ef3baedd798cc4fd085980ad9/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/b4f3095c1909924cbc98889a86c83d6d/events.json +131 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/b54b7fc32b7575a7110de809d11ccda0/events.json +128 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/c0b48033fa06746bcc5886745e053cff/events.json +169 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/c44649ac76701b4558927cd2305ab535/events.json +169 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/d601ae3320057580a39dbdac2edfdf4a/events.json +248 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/e67e724bab422d2b52eeb49635e512e1/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/e6c72765a28f158a8485b35fa63f73da/events.json +194 -0
- data/gems/e11y-devtools/frontend/public/mocks/v1/traces/f541b87405c9a54819b18ebe529f6419/events.json +194 -0
- data/gems/e11y-devtools/frontend/scripts/generate_mocks.rb +397 -0
- data/gems/e11y-devtools/frontend/src/App.svelte +827 -0
- data/gems/e11y-devtools/frontend/src/components/Fab.svelte +19 -0
- data/gems/e11y-devtools/frontend/src/components/FilterBar.svelte +38 -0
- data/gems/e11y-devtools/frontend/src/components/FullscreenPanel.svelte +82 -0
- data/gems/e11y-devtools/frontend/src/components/InteractionsTimeline.svelte +264 -0
- data/gems/e11y-devtools/frontend/src/components/RecentHistogram.svelte +354 -0
- data/gems/e11y-devtools/frontend/src/lib/api.ts +37 -0
- data/gems/e11y-devtools/frontend/src/lib/eventIdentity.ts +12 -0
- data/gems/e11y-devtools/frontend/src/lib/format.ts +37 -0
- data/gems/e11y-devtools/frontend/src/lib/listFilter.ts +43 -0
- data/gems/e11y-devtools/frontend/src/lib/recentVolume.ts +80 -0
- data/gems/e11y-devtools/frontend/src/lib/router.ts +12 -0
- data/gems/e11y-devtools/frontend/src/lib/transitions.ts +34 -0
- data/gems/e11y-devtools/frontend/src/lib/viewportOrigin.ts +25 -0
- data/gems/e11y-devtools/frontend/src/main.ts +8 -0
- data/gems/e11y-devtools/frontend/src/overlay-entry.ts +24 -0
- data/gems/e11y-devtools/frontend/src/overlay.css +1080 -0
- data/gems/e11y-devtools/frontend/svelte.config.js +2 -0
- data/gems/e11y-devtools/frontend/test_puppeteer.js +41 -0
- data/gems/e11y-devtools/frontend/test_scale.js +3 -0
- data/gems/e11y-devtools/frontend/tsconfig.app.json +21 -0
- data/gems/e11y-devtools/frontend/tsconfig.json +7 -0
- data/gems/e11y-devtools/frontend/tsconfig.node.json +26 -0
- data/gems/e11y-devtools/frontend/vite.config.ts +36 -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 +20 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/controller.rb +94 -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 +67 -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 +91 -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 +44 -12
- data/lib/e11y/instruments/rails_instrumentation.rb +49 -24
- data/lib/e11y/instruments/sidekiq.rb +135 -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 +4 -4
- data/lib/e11y/presets/audit_event.rb +13 -2
- data/lib/e11y/railtie.rb +52 -14
- 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 +144 -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 +123 -266
- 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 +186 -39
- data/docs/ADR-003-slo-observability.md +0 -3337
- 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
|
@@ -10,15 +10,14 @@ module E11y
|
|
|
10
10
|
# **Unidirectional Flow:** ASN → E11y
|
|
11
11
|
#
|
|
12
12
|
# @example Basic usage
|
|
13
|
-
# # Automatically enabled by E11y::Railtie if config.
|
|
13
|
+
# # Automatically enabled by E11y::Railtie if config.rails_instrumentation_enabled = true
|
|
14
14
|
# E11y::Instruments::RailsInstrumentation.setup!
|
|
15
15
|
#
|
|
16
16
|
# @example Custom event mapping
|
|
17
17
|
# E11y.configure do |config|
|
|
18
|
-
# config.
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
# end
|
|
18
|
+
# config.rails_instrumentation_enabled = true
|
|
19
|
+
# config.rails_instrumentation_custom_mappings['sql.active_record'] = MyApp::CustomQueryEvent
|
|
20
|
+
# config.rails_instrumentation_ignore_events << 'cache_read.active_support'
|
|
22
21
|
# end
|
|
23
22
|
#
|
|
24
23
|
# @see ADR-008 §4.1 (Unidirectional Flow ASN → E11y)
|
|
@@ -41,6 +40,7 @@ module E11y
|
|
|
41
40
|
"enqueue.active_job" => "E11y::Events::Rails::Job::Enqueued",
|
|
42
41
|
"enqueue_at.active_job" => "E11y::Events::Rails::Job::Scheduled",
|
|
43
42
|
"perform_start.active_job" => "E11y::Events::Rails::Job::Started",
|
|
43
|
+
# perform.active_job: Completed on success, Failed on exception (routed in track_rails_event)
|
|
44
44
|
"perform.active_job" => "E11y::Events::Rails::Job::Completed"
|
|
45
45
|
}.freeze
|
|
46
46
|
|
|
@@ -50,7 +50,7 @@ module E11y
|
|
|
50
50
|
#
|
|
51
51
|
# @return [void]
|
|
52
52
|
def self.setup!
|
|
53
|
-
return unless E11y.config.
|
|
53
|
+
return unless E11y.config.rails_instrumentation_enabled
|
|
54
54
|
|
|
55
55
|
# Subscribe to each configured event pattern
|
|
56
56
|
event_mapping.each do |asn_pattern, e11y_event_class_name|
|
|
@@ -81,25 +81,50 @@ module E11y
|
|
|
81
81
|
# # Result: { controller: "Users", action: "index" } - password filtered by schema
|
|
82
82
|
def self.subscribe_to_event(asn_pattern, e11y_event_class_name)
|
|
83
83
|
ActiveSupport::Notifications.subscribe(asn_pattern) do |name, start, finish, _id, payload|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
track_rails_event(name, start, finish, payload, e11y_event_class_name)
|
|
85
|
+
rescue StandardError => e
|
|
86
|
+
warn "[E11y] Failed to track Rails event #{name}: #{e.message}"
|
|
87
|
+
end
|
|
88
|
+
end
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
def self.track_rails_event(name, start, finish, payload, e11y_event_class_name)
|
|
91
|
+
duration = (finish - start) * 1000
|
|
92
|
+
extracted_payload = extract_job_info_from_object(payload)
|
|
93
|
+
|
|
94
|
+
# perform.active_job: route to Failed when job raised exception
|
|
95
|
+
if name == "perform.active_job" && job_failed?(payload)
|
|
96
|
+
e11y_event_class = resolve_event_class("E11y::Events::Rails::Job::Failed")
|
|
97
|
+
extracted_payload = extracted_payload.merge(extract_job_exception_info(payload))
|
|
98
|
+
else
|
|
88
99
|
e11y_event_class = resolve_event_class(e11y_event_class_name)
|
|
89
|
-
|
|
100
|
+
extracted_payload = extracted_payload.merge(severity: :error) if process_action_error?(name, payload)
|
|
101
|
+
end
|
|
90
102
|
|
|
91
|
-
|
|
92
|
-
extracted_payload = extract_job_info_from_object(payload)
|
|
103
|
+
return unless e11y_event_class
|
|
93
104
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
105
|
+
e11y_event_class.track(event_name: name, duration: duration, **extracted_payload)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def self.process_action_error?(name, payload)
|
|
109
|
+
name == "process_action.action_controller" && (payload[:exception] || payload["exception"])
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def self.job_failed?(payload)
|
|
113
|
+
payload[:exception].present? || payload["exception"].present?
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Extract error_class and error_message from ActiveJob exception payload.
|
|
117
|
+
# Rails passes exception as ["ErrorClass", "message"] or exception_object.
|
|
118
|
+
def self.extract_job_exception_info(payload)
|
|
119
|
+
ex = payload[:exception] || payload["exception"]
|
|
120
|
+
return {} unless ex
|
|
121
|
+
|
|
122
|
+
if ex.is_a?(Array) && ex.size >= 2
|
|
123
|
+
{ error_class: ex[0].to_s, error_message: ex[1].to_s }
|
|
124
|
+
elsif ex.respond_to?(:class) && ex.respond_to?(:message)
|
|
125
|
+
{ error_class: ex.class.name, error_message: ex.message.to_s }
|
|
126
|
+
else
|
|
127
|
+
{}
|
|
103
128
|
end
|
|
104
129
|
end
|
|
105
130
|
|
|
@@ -110,9 +135,9 @@ module E11y
|
|
|
110
135
|
mapping = DEFAULT_RAILS_EVENT_MAPPING.dup
|
|
111
136
|
|
|
112
137
|
# Apply custom mappings from config (Devise-style overrides)
|
|
113
|
-
custom_mappings = E11y.config.
|
|
138
|
+
custom_mappings = E11y.config.rails_instrumentation_custom_mappings || {}
|
|
114
139
|
custom_mappings.each do |pattern, event_class|
|
|
115
|
-
mapping[pattern] = event_class.name
|
|
140
|
+
mapping[pattern] = event_class.respond_to?(:name) ? event_class.name : event_class.to_s
|
|
116
141
|
end
|
|
117
142
|
|
|
118
143
|
mapping
|
|
@@ -123,7 +148,7 @@ module E11y
|
|
|
123
148
|
# @param pattern [String] ASN event pattern
|
|
124
149
|
# @return [Boolean] true if should be ignored
|
|
125
150
|
def self.ignored?(pattern)
|
|
126
|
-
ignore_list = E11y.config.
|
|
151
|
+
ignore_list = E11y.config.rails_instrumentation_ignore_events || []
|
|
127
152
|
ignore_list.include?(pattern)
|
|
128
153
|
end
|
|
129
154
|
|
|
@@ -23,62 +23,151 @@ module E11y
|
|
|
23
23
|
#
|
|
24
24
|
# @see ADR-008 §9 (Sidekiq Integration)
|
|
25
25
|
module Sidekiq
|
|
26
|
+
# Shared helper: detect raw Sidekiq jobs (not ActiveJob-wrapped)
|
|
27
|
+
module RawSidekiqJob
|
|
28
|
+
def raw_sidekiq_job?(job)
|
|
29
|
+
job_class = job["class"].to_s
|
|
30
|
+
return false if job_class.include?("ActiveJob::QueueAdapters::SidekiqAdapter")
|
|
31
|
+
return false if job["wrapped"].present?
|
|
32
|
+
|
|
33
|
+
true
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Emits job lifecycle events (Started, Completed, Failed) for ServerMiddleware
|
|
38
|
+
module JobEventEmitter
|
|
39
|
+
def emit_job_started(job, queue)
|
|
40
|
+
Events::Rails::Job::Started.track(
|
|
41
|
+
event_name: "sidekiq.perform_start",
|
|
42
|
+
duration: 0,
|
|
43
|
+
job_class: job["class"],
|
|
44
|
+
job_id: job["jid"],
|
|
45
|
+
queue: queue
|
|
46
|
+
)
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
warn "[E11y] Failed to emit job Started: #{e.message}"
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def emit_job_completed(job, queue, start_time)
|
|
52
|
+
duration_ms = ((Time.now - start_time) * 1000).round(2)
|
|
53
|
+
Events::Rails::Job::Completed.track(
|
|
54
|
+
event_name: "sidekiq.perform",
|
|
55
|
+
duration: duration_ms,
|
|
56
|
+
job_class: job["class"],
|
|
57
|
+
job_id: job["jid"],
|
|
58
|
+
queue: queue
|
|
59
|
+
)
|
|
60
|
+
rescue StandardError => e
|
|
61
|
+
warn "[E11y] Failed to emit job Completed: #{e.message}"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def emit_job_failed(job, queue, start_time, error)
|
|
65
|
+
duration_ms = ((Time.now - start_time) * 1000).round(2)
|
|
66
|
+
Events::Rails::Job::Failed.track(
|
|
67
|
+
event_name: "sidekiq.perform",
|
|
68
|
+
duration: duration_ms,
|
|
69
|
+
job_class: job["class"],
|
|
70
|
+
job_id: job["jid"],
|
|
71
|
+
queue: queue,
|
|
72
|
+
error_class: error.class.name,
|
|
73
|
+
error_message: error.message
|
|
74
|
+
)
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
warn "[E11y] Failed to emit job Failed: #{e.message}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
26
80
|
# Client-side middleware: Inject trace context when enqueueing job
|
|
27
81
|
#
|
|
28
82
|
# **C17 Hybrid Tracing**: Propagates parent_trace_id to job metadata.
|
|
29
83
|
# Job will create NEW trace_id but keep link to parent.
|
|
84
|
+
#
|
|
85
|
+
# **Job lifecycle events**: Emits Events::Rails::Job::Enqueued for raw Sidekiq jobs only.
|
|
86
|
+
# ActiveJob jobs are handled by RailsInstrumentation (ASN).
|
|
30
87
|
class ClientMiddleware
|
|
31
|
-
|
|
88
|
+
include RawSidekiqJob
|
|
89
|
+
|
|
90
|
+
def call(worker_class, job, queue, _redis_pool)
|
|
32
91
|
# Inject current trace context into job metadata as parent trace
|
|
33
92
|
# Job will generate NEW trace_id but keep parent link (C17)
|
|
34
93
|
job["e11y_parent_trace_id"] = E11y::Current.trace_id if E11y::Current.trace_id
|
|
35
94
|
job["e11y_parent_span_id"] = E11y::Current.span_id if E11y::Current.span_id
|
|
95
|
+
job["e11y_sampled"] = E11y::Current.sampled if E11y::Current.respond_to?(:sampled) && !E11y::Current.sampled.nil?
|
|
96
|
+
baggage = E11y::Tracing::Propagator.baggage_for_propagation_from_current
|
|
97
|
+
job["e11y_baggage"] = baggage if baggage.any?
|
|
98
|
+
|
|
99
|
+
# Emit Enqueued for raw Sidekiq jobs only (ActiveJob emits via ASN)
|
|
100
|
+
emit_job_enqueued(worker_class, job, queue) if raw_sidekiq_job?(job)
|
|
36
101
|
|
|
37
102
|
yield
|
|
38
103
|
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def emit_job_enqueued(worker_class, job, queue)
|
|
108
|
+
Events::Rails::Job::Enqueued.track(
|
|
109
|
+
event_name: "sidekiq.enqueue",
|
|
110
|
+
duration: 0,
|
|
111
|
+
job_class: worker_class.to_s,
|
|
112
|
+
job_id: job["jid"],
|
|
113
|
+
queue: queue
|
|
114
|
+
)
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
warn "[E11y] Failed to emit job Enqueued: #{e.message}"
|
|
117
|
+
end
|
|
39
118
|
end
|
|
40
119
|
|
|
41
120
|
# Server-side middleware: Set up job-scoped context when executing job
|
|
42
121
|
#
|
|
43
122
|
# **C17 Hybrid Tracing**: Creates NEW trace_id for job, but preserves parent link.
|
|
44
123
|
# **C18 Non-Failing**: E11y errors don't fail jobs (observability is secondary to business logic).
|
|
124
|
+
#
|
|
125
|
+
# **Job lifecycle events**: Emits Events::Rails::Job::Started/Completed/Failed for raw Sidekiq jobs only.
|
|
126
|
+
# ActiveJob jobs (when Sidekiq is the queue adapter) are handled by RailsInstrumentation (ASN).
|
|
45
127
|
class ServerMiddleware
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
# C18: Disable fail_on_error for jobs (observability should not block business logic)
|
|
49
|
-
original_fail_on_error = E11y.config.error_handling.fail_on_error
|
|
50
|
-
E11y.config.error_handling.fail_on_error = false
|
|
51
|
-
|
|
52
|
-
setup_job_context(job)
|
|
53
|
-
setup_job_buffer
|
|
128
|
+
include RawSidekiqJob
|
|
129
|
+
include JobEventEmitter
|
|
54
130
|
|
|
55
|
-
|
|
131
|
+
def call(_worker, job, queue)
|
|
132
|
+
original_fail_on_error = disable_fail_on_error
|
|
56
133
|
start_time = Time.now
|
|
57
134
|
job_status = :success
|
|
58
135
|
|
|
59
|
-
|
|
136
|
+
setup_job_context(job, queue)
|
|
137
|
+
setup_job_buffer
|
|
138
|
+
|
|
139
|
+
emit_job_started(job, queue) if raw_sidekiq_job?(job)
|
|
60
140
|
yield
|
|
61
141
|
rescue StandardError => e
|
|
62
142
|
job_status = :failed
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
raise # Always re-raise original exception
|
|
143
|
+
on_job_exception(job, queue, start_time, e)
|
|
144
|
+
raise
|
|
67
145
|
ensure
|
|
68
|
-
|
|
69
|
-
|
|
146
|
+
finalize_job(job, queue, start_time, job_status, original_fail_on_error)
|
|
147
|
+
end
|
|
70
148
|
|
|
71
|
-
|
|
149
|
+
private
|
|
72
150
|
|
|
73
|
-
|
|
74
|
-
E11y.config.
|
|
151
|
+
def disable_fail_on_error
|
|
152
|
+
original = E11y.config.error_handling_fail_on_error
|
|
153
|
+
E11y.config.error_handling_fail_on_error = false
|
|
154
|
+
original
|
|
75
155
|
end
|
|
76
|
-
# rubocop:enable Metrics/AbcSize
|
|
77
156
|
|
|
78
|
-
|
|
157
|
+
def on_job_exception(job, queue, start_time, error)
|
|
158
|
+
emit_job_failed(job, queue, start_time, error) if raw_sidekiq_job?(job)
|
|
159
|
+
handle_job_error(error)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def finalize_job(job, queue, start_time, job_status, original_fail_on_error)
|
|
163
|
+
emit_job_completed(job, queue, start_time) if raw_sidekiq_job?(job) && job_status == :success
|
|
164
|
+
track_job_slo(job, queue, job_status, start_time)
|
|
165
|
+
cleanup_job_context
|
|
166
|
+
E11y.config.error_handling_fail_on_error = original_fail_on_error
|
|
167
|
+
end
|
|
79
168
|
|
|
80
169
|
# Setup job-scoped context (C17 Hybrid Tracing)
|
|
81
|
-
def setup_job_context(job)
|
|
170
|
+
def setup_job_context(job, queue = nil)
|
|
82
171
|
# Extract parent trace context from job metadata
|
|
83
172
|
parent_trace_id = job["e11y_parent_trace_id"]
|
|
84
173
|
|
|
@@ -91,13 +180,28 @@ module E11y
|
|
|
91
180
|
E11y::Current.span_id = span_id
|
|
92
181
|
E11y::Current.parent_trace_id = parent_trace_id
|
|
93
182
|
E11y::Current.request_id = job["jid"]
|
|
183
|
+
E11y::Tracing::Propagator.hydrate_current_from_job_baggage!(job["e11y_baggage"]) if job.key?("e11y_baggage")
|
|
184
|
+
|
|
185
|
+
# Restore or compute sampling decision (ADR-005 §7)
|
|
186
|
+
if job.key?("e11y_sampled")
|
|
187
|
+
E11y::Current.sampled = job["e11y_sampled"]
|
|
188
|
+
else
|
|
189
|
+
require "e11y/trace_context/sampler"
|
|
190
|
+
ctx = E11y::Current.to_context.merge(
|
|
191
|
+
job_class: job["class"],
|
|
192
|
+
queue: queue
|
|
193
|
+
).compact
|
|
194
|
+
E11y::Current.sampled = E11y::TraceContext::Sampler.should_sample?(ctx)
|
|
195
|
+
end
|
|
94
196
|
end
|
|
95
197
|
|
|
96
|
-
# Setup
|
|
198
|
+
# Setup request-scoped buffer (same as HTTP; optional job_buffer_limit)
|
|
97
199
|
def setup_job_buffer
|
|
98
|
-
return unless E11y.config.
|
|
200
|
+
return unless E11y.config.ephemeral_buffer_enabled
|
|
99
201
|
|
|
100
|
-
E11y
|
|
202
|
+
limit = E11y.config.ephemeral_buffer_job_buffer_limit ||
|
|
203
|
+
E11y::Buffers::EphemeralBuffer::DEFAULT_BUFFER_LIMIT
|
|
204
|
+
E11y::Buffers::EphemeralBuffer.initialize!(buffer_limit: limit)
|
|
101
205
|
rescue StandardError => e
|
|
102
206
|
# C18: Don't fail job if buffer setup fails
|
|
103
207
|
warn "[E11y] Failed to start job buffer: #{e.message}"
|
|
@@ -106,9 +210,9 @@ module E11y
|
|
|
106
210
|
# Handle job error (C18: Non-Failing Event Tracking)
|
|
107
211
|
def handle_job_error(_error)
|
|
108
212
|
# Flush buffer on error (includes debug events)
|
|
109
|
-
return unless E11y.config.
|
|
213
|
+
return unless E11y.config.ephemeral_buffer_enabled
|
|
110
214
|
|
|
111
|
-
E11y::Buffers::
|
|
215
|
+
E11y::Buffers::EphemeralBuffer.flush_on_error
|
|
112
216
|
rescue StandardError => e
|
|
113
217
|
# C18: Don't fail job if buffer flush fails
|
|
114
218
|
warn "[E11y] Failed to flush job buffer on error: #{e.message}"
|
|
@@ -117,9 +221,9 @@ module E11y
|
|
|
117
221
|
# Cleanup job-scoped context
|
|
118
222
|
def cleanup_job_context
|
|
119
223
|
# Discard buffer on success (not on error, already flushed in rescue)
|
|
120
|
-
if !$ERROR_INFO && E11y.config.
|
|
224
|
+
if !$ERROR_INFO && E11y.config.ephemeral_buffer_enabled
|
|
121
225
|
begin
|
|
122
|
-
E11y::Buffers::
|
|
226
|
+
E11y::Buffers::EphemeralBuffer.discard
|
|
123
227
|
rescue StandardError => e
|
|
124
228
|
# C18: Don't fail job if buffer flush fails
|
|
125
229
|
warn "[E11y] Failed to flush job buffer: #{e.message}"
|
|
@@ -154,7 +258,7 @@ module E11y
|
|
|
154
258
|
# @return [void]
|
|
155
259
|
# @api private
|
|
156
260
|
def track_job_slo(job, queue, status, start_time)
|
|
157
|
-
return unless E11y.config.
|
|
261
|
+
return unless E11y.config.respond_to?(:slo_tracking_enabled) && E11y.config.slo_tracking_enabled
|
|
158
262
|
|
|
159
263
|
duration_ms = ((Time.now - start_time) * 1000).round(2)
|
|
160
264
|
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "e11y/linters/base"
|
|
4
|
+
require "e11y/registry"
|
|
5
|
+
|
|
6
|
+
module E11y
|
|
7
|
+
module Linters
|
|
8
|
+
module PII
|
|
9
|
+
# Linter for explicit PII declaration on Event classes.
|
|
10
|
+
#
|
|
11
|
+
# When an event declares `contains_pii true`, every schema field must have
|
|
12
|
+
# an explicit PII strategy in the pii_filtering block.
|
|
13
|
+
#
|
|
14
|
+
# @see ADR-006 §3.0.5 PII Declaration Linter
|
|
15
|
+
# @see UC-007 PII Filtering
|
|
16
|
+
class PiiDeclarationLinter
|
|
17
|
+
VALID_STRATEGIES = %i[allow skip mask hash redact partial truncate encrypt].freeze
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
# Validate all registered event classes.
|
|
21
|
+
#
|
|
22
|
+
# @raise [E11y::Linters::PiiDeclarationError] when any event with contains_pii true has missing/invalid declarations
|
|
23
|
+
def validate_all!
|
|
24
|
+
errors = []
|
|
25
|
+
|
|
26
|
+
E11y::Registry.event_classes.each do |event_class|
|
|
27
|
+
validate!(event_class)
|
|
28
|
+
rescue PiiDeclarationError => e
|
|
29
|
+
errors << e.message
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
raise PiiDeclarationError, errors.join("\n\n") if errors.any?
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Validate a single event class.
|
|
36
|
+
#
|
|
37
|
+
# @param event_class [Class] Event class to validate
|
|
38
|
+
# @raise [E11y::Linters::PiiDeclarationError] when validation fails
|
|
39
|
+
def validate!(event_class)
|
|
40
|
+
return unless event_class.contains_pii == true
|
|
41
|
+
|
|
42
|
+
schema_fields = extract_schema_keys(event_class)
|
|
43
|
+
return if schema_fields.nil? || schema_fields.empty?
|
|
44
|
+
|
|
45
|
+
pii_config = event_class.pii_filtering_config
|
|
46
|
+
declared_fields = pii_config&.dig(:fields)&.keys&.map(&:to_s) || []
|
|
47
|
+
|
|
48
|
+
missing = schema_fields.map(&:to_s) - declared_fields
|
|
49
|
+
raise PiiDeclarationError, build_missing_message(event_class, missing) if missing.any?
|
|
50
|
+
|
|
51
|
+
# Validate each declared field has valid strategy
|
|
52
|
+
pii_config[:fields].each do |field, config|
|
|
53
|
+
validate_field_config!(event_class, field, config)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def extract_schema_keys(klass)
|
|
60
|
+
return nil unless klass.respond_to?(:compiled_schema)
|
|
61
|
+
|
|
62
|
+
schema = klass.compiled_schema
|
|
63
|
+
return nil unless schema.respond_to?(:key_map)
|
|
64
|
+
|
|
65
|
+
schema.key_map.keys.map(&:name)
|
|
66
|
+
rescue StandardError
|
|
67
|
+
nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def build_missing_message(event_class, missing_fields)
|
|
71
|
+
fields_snippet = missing_fields.map do |f|
|
|
72
|
+
" field :#{f} do\n strategy :mask # or :hash, :allow, :redact\n end"
|
|
73
|
+
end.join("\n ")
|
|
74
|
+
|
|
75
|
+
<<~ERROR
|
|
76
|
+
PII Declaration Error: #{event_class.name}
|
|
77
|
+
|
|
78
|
+
Event declared `contains_pii true` but missing field declarations:
|
|
79
|
+
|
|
80
|
+
Missing fields: #{missing_fields.map { |x| ":#{x}" }.join(', ')}
|
|
81
|
+
|
|
82
|
+
Fix: Add explicit PII strategy for each field in pii_filtering block:
|
|
83
|
+
|
|
84
|
+
class #{event_class.name} < E11y::Event::Base
|
|
85
|
+
contains_pii true
|
|
86
|
+
|
|
87
|
+
pii_filtering do
|
|
88
|
+
#{fields_snippet}
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
Available strategies: #{VALID_STRATEGIES.map { |s| ":#{s}" }.join(', ')}
|
|
93
|
+
ERROR
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def validate_field_config!(event_class, field, config)
|
|
97
|
+
strategy = config[:strategy]
|
|
98
|
+
unless VALID_STRATEGIES.include?(strategy)
|
|
99
|
+
raise PiiDeclarationError, <<~ERROR
|
|
100
|
+
Invalid PII strategy for #{event_class.name}##{field}
|
|
101
|
+
|
|
102
|
+
Strategy: #{strategy.inspect}
|
|
103
|
+
Valid strategies: #{VALID_STRATEGIES.map { |s| ":#{s}" }.join(', ')}
|
|
104
|
+
ERROR
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
return unless config.key?(:exclude_adapters)
|
|
108
|
+
|
|
109
|
+
return if config[:exclude_adapters].is_a?(Array)
|
|
110
|
+
|
|
111
|
+
raise PiiDeclarationError, "exclude_adapters must be an Array for #{event_class.name}##{field}"
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Raised when PII declaration validation fails.
|
|
117
|
+
class PiiDeclarationError < LinterError; end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "e11y/linters/base"
|
|
4
|
+
require "e11y/slo/config_loader"
|
|
5
|
+
|
|
6
|
+
module E11y
|
|
7
|
+
module Linters
|
|
8
|
+
module SLO
|
|
9
|
+
# Linter for slo.yml custom_slos consistency with Event class definitions.
|
|
10
|
+
#
|
|
11
|
+
# Ensures every event referenced in slo.yml custom_slos:
|
|
12
|
+
# - Exists (constantize succeeds)
|
|
13
|
+
# - Has slo enabled (slo_enabled?)
|
|
14
|
+
# - Has contributes_to matching the slo_name in config
|
|
15
|
+
class ConfigConsistencyLinter
|
|
16
|
+
class << self
|
|
17
|
+
# Validate slo.yml custom_slos against Event class definitions.
|
|
18
|
+
#
|
|
19
|
+
# @param search_paths [Array<String>, nil] Optional search paths for ConfigLoader.
|
|
20
|
+
# When nil, ConfigLoader uses default paths.
|
|
21
|
+
# @raise [E11y::Linters::LinterError] when any event fails validation
|
|
22
|
+
def validate!(search_paths: nil)
|
|
23
|
+
config = if search_paths
|
|
24
|
+
E11y::SLO::ConfigLoader.load(search_paths: search_paths)
|
|
25
|
+
else
|
|
26
|
+
E11y::SLO::ConfigLoader.load
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
return if config.nil?
|
|
30
|
+
return if config["custom_slos"].nil? || config["custom_slos"].empty?
|
|
31
|
+
|
|
32
|
+
errors = []
|
|
33
|
+
|
|
34
|
+
config["custom_slos"].each do |slo|
|
|
35
|
+
slo_name = slo["name"]
|
|
36
|
+
events = slo["events"] || []
|
|
37
|
+
|
|
38
|
+
events.each do |event_class_name|
|
|
39
|
+
error = validate_event(slo_name, event_class_name)
|
|
40
|
+
errors << error if error
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
return if errors.empty?
|
|
45
|
+
|
|
46
|
+
raise LinterError, errors.join("\n")
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def validate_event(slo_name, event_class_name)
|
|
52
|
+
event_class = constantize_event(event_class_name)
|
|
53
|
+
return "Event class '#{event_class_name}' does not exist (constantize failed)" if event_class.nil?
|
|
54
|
+
|
|
55
|
+
unless event_class.respond_to?(:slo_enabled?) && event_class.slo_enabled?
|
|
56
|
+
return "Event #{event_class_name} is referenced in slo.yml (SLO '#{slo_name}') but has slo disabled"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
contributes_to = event_class.slo_config&.contributes_to_value
|
|
60
|
+
unless contributes_to == slo_name
|
|
61
|
+
return "Event #{event_class_name} contributes_to '#{contributes_to}' but slo.yml defines SLO '#{slo_name}'"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def constantize_event(event_class_name)
|
|
68
|
+
Object.const_get(event_class_name)
|
|
69
|
+
rescue NameError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "e11y/linters/base"
|
|
4
|
+
require "e11y/registry"
|
|
5
|
+
|
|
6
|
+
module E11y
|
|
7
|
+
module Linters
|
|
8
|
+
module SLO
|
|
9
|
+
# Linter for explicit SLO declaration on Event classes.
|
|
10
|
+
#
|
|
11
|
+
# Ensures every registered event class has either `slo do ... end` or
|
|
12
|
+
# `slo false` — i.e. slo_enabled? or slo_disabled? must be true.
|
|
13
|
+
class ExplicitDeclarationLinter
|
|
14
|
+
class << self
|
|
15
|
+
# Validate all registered event classes have explicit SLO declaration.
|
|
16
|
+
#
|
|
17
|
+
# @raise [E11y::Linters::LinterError] when any event has neither slo_enabled? nor slo_disabled?
|
|
18
|
+
def validate!
|
|
19
|
+
errors = []
|
|
20
|
+
|
|
21
|
+
E11y::Registry.event_classes.each do |event_class|
|
|
22
|
+
next if event_class.slo_enabled? || event_class.slo_disabled?
|
|
23
|
+
|
|
24
|
+
name = event_class.respond_to?(:event_name) ? event_class.event_name : event_class.name
|
|
25
|
+
errors << "Event #{name} missing explicit SLO declaration! Add `slo do ... end` or `slo false`"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
return if errors.empty?
|
|
29
|
+
|
|
30
|
+
raise LinterError, errors.join("\n")
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "e11y/linters/base"
|
|
4
|
+
require "e11y/registry"
|
|
5
|
+
|
|
6
|
+
module E11y
|
|
7
|
+
module Linters
|
|
8
|
+
module SLO
|
|
9
|
+
# Linter for SLO-enabled events: requires slo_status_from and contributes_to.
|
|
10
|
+
#
|
|
11
|
+
# When an event has slo_enabled?, it must define:
|
|
12
|
+
# - slo_status_from (slo_config.slo_status_proc) — how to compute slo_status from payload
|
|
13
|
+
# - contributes_to (slo_config.contributes_to_value) — which custom SLO this event feeds
|
|
14
|
+
class SloStatusFromLinter
|
|
15
|
+
class << self
|
|
16
|
+
# Validate all SLO-enabled event classes have slo_status_from and contributes_to.
|
|
17
|
+
#
|
|
18
|
+
# @raise [E11y::Linters::LinterError] when any slo-enabled event is missing either
|
|
19
|
+
def validate!
|
|
20
|
+
errors = []
|
|
21
|
+
|
|
22
|
+
E11y::Registry.event_classes.each do |event_class|
|
|
23
|
+
next unless event_class.slo_enabled?
|
|
24
|
+
|
|
25
|
+
config = event_class.slo_config
|
|
26
|
+
name = event_class.respond_to?(:event_name) ? event_class.event_name : event_class.name
|
|
27
|
+
|
|
28
|
+
errors << "Event #{name} has slo enabled but missing slo_status_from" unless config&.slo_status_proc
|
|
29
|
+
|
|
30
|
+
errors << "Event #{name} has slo enabled but missing contributes_to" unless config&.contributes_to_value
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
return if errors.empty?
|
|
34
|
+
|
|
35
|
+
raise LinterError, errors.join("\n")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|