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
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Middleware
|
|
5
|
+
# Measures Event.track() latency from pipeline entry to exit.
|
|
6
|
+
#
|
|
7
|
+
# Must be the FIRST middleware so it wraps the entire pipeline.
|
|
8
|
+
# Records duration for both success and dropped events.
|
|
9
|
+
#
|
|
10
|
+
# @see ADR-016 §3.1 (Performance Metrics)
|
|
11
|
+
# @example Add first in pipeline
|
|
12
|
+
# config.pipeline.use E11y::Middleware::TrackLatency
|
|
13
|
+
# config.pipeline.use E11y::Middleware::TraceContext
|
|
14
|
+
# # ...
|
|
15
|
+
class TrackLatency < Base
|
|
16
|
+
middleware_zone :pre_processing
|
|
17
|
+
|
|
18
|
+
def call(event_data)
|
|
19
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
20
|
+
result = @app.call(event_data)
|
|
21
|
+
duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
|
|
22
|
+
|
|
23
|
+
E11y::SelfMonitoring::PerformanceMonitor.track_latency(
|
|
24
|
+
duration_ms,
|
|
25
|
+
event_class: event_data[:event_name].to_s,
|
|
26
|
+
severity: event_data[:severity].to_s,
|
|
27
|
+
result: result.nil? ? :dropped : :success
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -56,7 +56,7 @@ module E11y
|
|
|
56
56
|
# @option event_data [Hash] :payload The event payload (required)
|
|
57
57
|
# @return [Hash, nil] Validated event data, or nil if dropped
|
|
58
58
|
# @raise [E11y::ValidationError] if validation fails
|
|
59
|
-
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
59
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
60
60
|
def call(event_data)
|
|
61
61
|
# Skip validation if no event_class or payload
|
|
62
62
|
return @app.call(event_data) unless event_data[:event_class] && event_data[:payload]
|
|
@@ -69,7 +69,7 @@ module E11y
|
|
|
69
69
|
|
|
70
70
|
# Skip validation if mode is :never
|
|
71
71
|
if validation_mode == :never
|
|
72
|
-
|
|
72
|
+
E11y::Metrics.increment(:e11y_middleware_validation_total, result: "skipped")
|
|
73
73
|
return @app.call(event_data)
|
|
74
74
|
end
|
|
75
75
|
|
|
@@ -77,7 +77,7 @@ module E11y
|
|
|
77
77
|
if validation_mode == :sampled
|
|
78
78
|
sample_rate = event_class.respond_to?(:validation_sample_rate) ? event_class.validation_sample_rate : 0.01
|
|
79
79
|
if rand >= sample_rate
|
|
80
|
-
|
|
80
|
+
E11y::Metrics.increment(:e11y_middleware_validation_total, result: "skipped")
|
|
81
81
|
return @app.call(event_data)
|
|
82
82
|
end
|
|
83
83
|
end
|
|
@@ -87,7 +87,7 @@ module E11y
|
|
|
87
87
|
|
|
88
88
|
# Skip validation if no schema defined (schema-less events)
|
|
89
89
|
unless schema
|
|
90
|
-
|
|
90
|
+
E11y::Metrics.increment(:e11y_middleware_validation_total, result: "skipped")
|
|
91
91
|
return @app.call(event_data)
|
|
92
92
|
end
|
|
93
93
|
|
|
@@ -96,17 +96,17 @@ module E11y
|
|
|
96
96
|
|
|
97
97
|
if result.success?
|
|
98
98
|
# Validation passed
|
|
99
|
-
|
|
99
|
+
E11y::Metrics.increment(:e11y_middleware_validation_total, result: "passed")
|
|
100
100
|
@app.call(event_data)
|
|
101
101
|
else
|
|
102
102
|
# Validation failed - raise error with details
|
|
103
|
-
|
|
103
|
+
E11y::Metrics.increment(:e11y_middleware_validation_total, result: "failed")
|
|
104
104
|
|
|
105
105
|
error_message = format_validation_errors(event_class, result.errors)
|
|
106
106
|
raise E11y::ValidationError, error_message
|
|
107
107
|
end
|
|
108
108
|
end
|
|
109
|
-
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
109
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
110
110
|
|
|
111
111
|
private
|
|
112
112
|
|
|
@@ -122,15 +122,6 @@ module E11y
|
|
|
122
122
|
|
|
123
123
|
"Validation failed for #{event_class.name}: #{error_details}"
|
|
124
124
|
end
|
|
125
|
-
|
|
126
|
-
# Placeholder for metrics instrumentation.
|
|
127
|
-
#
|
|
128
|
-
# @param metric_name [String] Metric name
|
|
129
|
-
# @return [void]
|
|
130
|
-
def increment_metric(_metric_name)
|
|
131
|
-
# TODO: Integrate with Yabeda/Prometheus in Phase 2
|
|
132
|
-
# Yabeda.e11y.middleware_validation_passed.increment
|
|
133
|
-
end
|
|
134
125
|
end
|
|
135
126
|
end
|
|
136
127
|
end
|
|
@@ -54,43 +54,47 @@ module E11y
|
|
|
54
54
|
# @see ADR-012 for versioning architecture
|
|
55
55
|
# @see UC-020 for use cases
|
|
56
56
|
class Versioning < Base
|
|
57
|
-
|
|
58
|
-
VERSION_REGEX =
|
|
57
|
+
middleware_zone :pre_processing
|
|
58
|
+
VERSION_REGEX = E11y::Versioning::VersionExtractor::VERSION_REGEX
|
|
59
|
+
|
|
60
|
+
# Lazy cache: class name -> normalized event_name (per class, immutable)
|
|
61
|
+
NORMALIZED_CACHE = Concurrent::Map.new
|
|
59
62
|
|
|
60
63
|
# Process event and add version field if needed
|
|
61
64
|
#
|
|
62
65
|
# @param event_data [Hash] Event payload
|
|
63
66
|
# @return [Hash] Event data with version field (if > 1)
|
|
64
67
|
def call(event_data)
|
|
65
|
-
|
|
66
|
-
|
|
68
|
+
klass = event_data[:event_class]
|
|
69
|
+
class_name = klass&.name
|
|
67
70
|
|
|
68
|
-
|
|
71
|
+
version = event_data[:version].to_i
|
|
72
|
+
version = extract_version(class_name) if version <= 1
|
|
69
73
|
event_data[:v] = version if version > 1
|
|
70
74
|
|
|
71
|
-
#
|
|
72
|
-
|
|
75
|
+
# event_data[:event_name] set by Base; fallback to klass.event_name for minimal event_data (tests)
|
|
76
|
+
incoming = event_data[:event_name]
|
|
77
|
+
incoming = klass.event_name if incoming.nil? && klass.respond_to?(:event_name)
|
|
78
|
+
incoming = incoming.to_s
|
|
79
|
+
# Custom uses dot notation ("order.paid"); default from Base uses "::"
|
|
80
|
+
event_data[:event_name] = incoming != "" && !incoming.include?("::") ? incoming : normalized_for(klass)
|
|
73
81
|
|
|
74
|
-
event_data
|
|
82
|
+
@app&.call(event_data) || event_data
|
|
75
83
|
end
|
|
76
84
|
|
|
77
85
|
private
|
|
78
86
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
# @param class_name [String] Event class name (e.g., "Events::OrderPaidV2")
|
|
82
|
-
# @return [Integer] Version number (default: 1)
|
|
83
|
-
#
|
|
84
|
-
# @example
|
|
85
|
-
# extract_version("Events::OrderPaid") => 1
|
|
86
|
-
# extract_version("Events::OrderPaidV2") => 2
|
|
87
|
-
# extract_version("Events::OrderPaidV3") => 3
|
|
88
|
-
def extract_version(class_name)
|
|
89
|
-
return 1 unless class_name
|
|
87
|
+
def normalized_for(klass)
|
|
88
|
+
return unless klass
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
name = klass.name
|
|
91
|
+
return unless name
|
|
92
|
+
|
|
93
|
+
NORMALIZED_CACHE.fetch(name) { NORMALIZED_CACHE[name] = normalize_event_name(name) }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def extract_version(class_name)
|
|
97
|
+
E11y::Versioning::VersionExtractor.extract_version(class_name)
|
|
94
98
|
end
|
|
95
99
|
|
|
96
100
|
# Normalize event_name by removing version suffix
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module OpenTelemetry
|
|
5
|
+
# Semantic conventions mapper for OTel attributes (ADR-007 §4, F4).
|
|
6
|
+
#
|
|
7
|
+
# Maps E11y payload keys to OpenTelemetry semantic convention attribute names.
|
|
8
|
+
# When event_name matches a convention type (http, database, etc.), known keys
|
|
9
|
+
# are mapped to semantic names (e.g. method → http.method).
|
|
10
|
+
#
|
|
11
|
+
# @see https://opentelemetry.io/docs/specs/semconv/
|
|
12
|
+
class SemanticConventions
|
|
13
|
+
# Key mappings by convention type
|
|
14
|
+
# https://opentelemetry.io/docs/specs/semconv/http/
|
|
15
|
+
# https://opentelemetry.io/docs/specs/semconv/database/
|
|
16
|
+
# https://opentelemetry.io/docs/specs/semconv/exceptions/
|
|
17
|
+
CONVENTIONS = {
|
|
18
|
+
http: {
|
|
19
|
+
"method" => "http.method",
|
|
20
|
+
"route" => "http.route",
|
|
21
|
+
"path" => "http.target",
|
|
22
|
+
"status_code" => "http.status_code",
|
|
23
|
+
"status" => "http.status_code",
|
|
24
|
+
"duration_ms" => "http.server.duration",
|
|
25
|
+
"request_size" => "http.request.body.size",
|
|
26
|
+
"response_size" => "http.response.body.size",
|
|
27
|
+
"user_agent" => "http.user_agent",
|
|
28
|
+
"client_ip" => "http.client_ip",
|
|
29
|
+
"scheme" => "http.scheme",
|
|
30
|
+
"host" => "http.host",
|
|
31
|
+
"server_name" => "http.server_name"
|
|
32
|
+
},
|
|
33
|
+
database: {
|
|
34
|
+
"query" => "db.statement",
|
|
35
|
+
"statement" => "db.statement",
|
|
36
|
+
"duration_ms" => "db.operation.duration",
|
|
37
|
+
"rows_affected" => "db.operation.rows_affected",
|
|
38
|
+
"connection_id" => "db.connection.id",
|
|
39
|
+
"database_name" => "db.name",
|
|
40
|
+
"table_name" => "db.sql.table",
|
|
41
|
+
"operation" => "db.operation"
|
|
42
|
+
},
|
|
43
|
+
rpc: {
|
|
44
|
+
"service" => "rpc.service",
|
|
45
|
+
"method" => "rpc.method",
|
|
46
|
+
"system" => "rpc.system",
|
|
47
|
+
"status_code" => "rpc.grpc.status_code"
|
|
48
|
+
},
|
|
49
|
+
messaging: {
|
|
50
|
+
"queue_name" => "messaging.destination.name",
|
|
51
|
+
"message_id" => "messaging.message.id",
|
|
52
|
+
"conversation_id" => "messaging.message.conversation_id",
|
|
53
|
+
"payload_size" => "messaging.message.payload_size_bytes",
|
|
54
|
+
"operation" => "messaging.operation"
|
|
55
|
+
},
|
|
56
|
+
exception: {
|
|
57
|
+
"error_type" => "exception.type",
|
|
58
|
+
"error_message" => "exception.message",
|
|
59
|
+
"error_class" => "exception.type",
|
|
60
|
+
"stacktrace" => "exception.stacktrace"
|
|
61
|
+
}
|
|
62
|
+
}.freeze
|
|
63
|
+
|
|
64
|
+
# Map payload keys to OTel semantic attribute names.
|
|
65
|
+
#
|
|
66
|
+
# @param event_name [String] Event name (used to detect convention type)
|
|
67
|
+
# @param payload [Hash] Event payload
|
|
68
|
+
# @return [Hash] Mapped payload with semantic keys where applicable
|
|
69
|
+
def self.map(event_name, payload)
|
|
70
|
+
convention_type = detect_convention_type(event_name)
|
|
71
|
+
return payload.transform_keys { |k| "event.#{k}" } unless convention_type
|
|
72
|
+
|
|
73
|
+
conventions = CONVENTIONS[convention_type]
|
|
74
|
+
payload.each_with_object({}) do |(key, value), mapped|
|
|
75
|
+
otel_key = conventions[key.to_s] || "event.#{key}"
|
|
76
|
+
mapped[otel_key] = value
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Map a single key to OTel semantic attribute name.
|
|
81
|
+
#
|
|
82
|
+
# @param event_name [String] Event name (used to detect convention type)
|
|
83
|
+
# @param key [String, Symbol] Payload key
|
|
84
|
+
# @return [String] OTel attribute key
|
|
85
|
+
def self.map_key(event_name, key)
|
|
86
|
+
convention_type = detect_convention_type(event_name)
|
|
87
|
+
return "event.#{key}" unless convention_type
|
|
88
|
+
|
|
89
|
+
conventions = CONVENTIONS[convention_type]
|
|
90
|
+
conventions[key.to_s] || "event.#{key}"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Detect convention type from event name
|
|
94
|
+
#
|
|
95
|
+
# @param event_name [String]
|
|
96
|
+
# @return [Symbol, nil]
|
|
97
|
+
def self.detect_convention_type(event_name)
|
|
98
|
+
name = event_name.to_s
|
|
99
|
+
return :http if name.match?(/http|request|response/i)
|
|
100
|
+
return :database if name.match?(/database|query|sql|postgres|mysql/i)
|
|
101
|
+
return :rpc if name.match?(/rpc|grpc/i)
|
|
102
|
+
return :messaging if name.match?(/message|queue|kafka|rabbitmq|sidekiq|job/i)
|
|
103
|
+
return :exception if name.match?(/error|exception|failure/i)
|
|
104
|
+
|
|
105
|
+
nil
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "e11y/opentelemetry/semantic_conventions"
|
|
4
|
+
|
|
5
|
+
module E11y
|
|
6
|
+
module OpenTelemetry
|
|
7
|
+
# Creates OpenTelemetry spans from E11y events (ADR-007 §6, F2).
|
|
8
|
+
#
|
|
9
|
+
# When enabled via config.opentelemetry_span_creation_patterns, creates
|
|
10
|
+
# OTel spans for matching events. Errors/fatal always create spans.
|
|
11
|
+
# Uses SemanticConventions for attribute mapping when applicable.
|
|
12
|
+
#
|
|
13
|
+
# @example Configuration
|
|
14
|
+
# E11y.configure do |config|
|
|
15
|
+
# config.opentelemetry_span_creation_patterns = ["order.*", "payment.*"]
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# @see ADR-007 §6 Traces Signal Export
|
|
19
|
+
# @see E11y::OpenTelemetry::SemanticConventions
|
|
20
|
+
class SpanCreator
|
|
21
|
+
ATTR_EVENT_NAME = "event.name"
|
|
22
|
+
ATTR_SEVERITY = "event.severity"
|
|
23
|
+
ATTR_E11Y_TRACE_ID = "e11y.trace_id"
|
|
24
|
+
ATTR_E11Y_SPAN_ID = "e11y.span_id"
|
|
25
|
+
|
|
26
|
+
class << self
|
|
27
|
+
def create_span_from_event(event_data)
|
|
28
|
+
return unless defined?(::OpenTelemetry::Trace)
|
|
29
|
+
return unless should_create_span?(event_data)
|
|
30
|
+
|
|
31
|
+
tracer = ::OpenTelemetry.tracer_provider.tracer("e11y", E11y::VERSION)
|
|
32
|
+
parent_ctx = ::OpenTelemetry::Context.current
|
|
33
|
+
start_ts = time_to_nano(event_data[:timestamp] || Time.now)
|
|
34
|
+
|
|
35
|
+
span = tracer.start_span(
|
|
36
|
+
span_name(event_data),
|
|
37
|
+
with_parent: parent_ctx,
|
|
38
|
+
kind: span_kind(event_data),
|
|
39
|
+
start_timestamp: start_ts
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
set_attributes(span, event_data)
|
|
43
|
+
set_status(span, event_data)
|
|
44
|
+
record_exception(span, event_data) if event_data[:severity].in?(%i[error fatal])
|
|
45
|
+
|
|
46
|
+
end_ts = compute_end_timestamp(event_data)
|
|
47
|
+
span.finish(end_timestamp: end_ts)
|
|
48
|
+
|
|
49
|
+
span
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def span_name(event_data)
|
|
55
|
+
event_data[:event_name].to_s.presence || "e11y.event"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def set_attributes(span, event_data)
|
|
59
|
+
span.set_attribute(ATTR_EVENT_NAME, event_data[:event_name].to_s)
|
|
60
|
+
span.set_attribute(ATTR_SEVERITY, event_data[:severity].to_s)
|
|
61
|
+
span.set_attribute(ATTR_E11Y_TRACE_ID, event_data[:trace_id].to_s) if event_data[:trace_id]
|
|
62
|
+
span.set_attribute(ATTR_E11Y_SPAN_ID, event_data[:span_id].to_s) if event_data[:span_id]
|
|
63
|
+
|
|
64
|
+
payload = event_data[:payload] || {}
|
|
65
|
+
return if payload.empty?
|
|
66
|
+
|
|
67
|
+
mapped = E11y::OpenTelemetry::SemanticConventions.map(event_data[:event_name].to_s, payload)
|
|
68
|
+
mapped.each do |key, value|
|
|
69
|
+
next if value.nil?
|
|
70
|
+
|
|
71
|
+
span.set_attribute(key.to_s, otel_value(value))
|
|
72
|
+
rescue ArgumentError, TypeError
|
|
73
|
+
span.set_attribute(key.to_s, value.to_s)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def otel_value(value)
|
|
78
|
+
case value
|
|
79
|
+
when TrueClass, FalseClass, Integer, Float, String then value
|
|
80
|
+
when Array then value.map(&:to_s)
|
|
81
|
+
else value.to_s # Symbol, NilClass, Hash, etc.
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def set_status(span, event_data)
|
|
86
|
+
if event_data[:severity].in?(%i[error fatal])
|
|
87
|
+
msg = event_data.dig(:payload, :error_message) ||
|
|
88
|
+
event_data.dig(:payload, "error_message") || "Error"
|
|
89
|
+
span.status = ::OpenTelemetry::Trace::Status.error(msg.to_s)
|
|
90
|
+
else
|
|
91
|
+
span.status = ::OpenTelemetry::Trace::Status.ok
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def record_exception(span, event_data)
|
|
96
|
+
exc = event_data[:exception] || event_data.dig(:payload, :exception) || event_data.dig(:payload, "exception")
|
|
97
|
+
span.record_exception(exc) if exc.is_a?(Exception)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def compute_end_timestamp(event_data)
|
|
101
|
+
start = event_data[:timestamp] || Time.now
|
|
102
|
+
start_ns = time_to_nano(start)
|
|
103
|
+
if event_data[:duration_ms]
|
|
104
|
+
start_ns + (event_data[:duration_ms].to_f * 1_000_000).to_i
|
|
105
|
+
else
|
|
106
|
+
time_to_nano(Time.now)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def should_create_span?(event_data)
|
|
111
|
+
return true if event_data[:severity].in?(%i[error fatal])
|
|
112
|
+
|
|
113
|
+
patterns = E11y.config&.opentelemetry_span_creation_patterns || []
|
|
114
|
+
event_name = event_data[:event_name].to_s
|
|
115
|
+
return false if event_name.empty?
|
|
116
|
+
|
|
117
|
+
patterns.any? { |p| File.fnmatch(p.to_s, event_name) }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def span_kind(event_data)
|
|
121
|
+
kind = (event_data[:span_kind] || :internal).to_sym
|
|
122
|
+
case kind
|
|
123
|
+
when :server then ::OpenTelemetry::Trace::SpanKind::SERVER
|
|
124
|
+
when :client then ::OpenTelemetry::Trace::SpanKind::CLIENT
|
|
125
|
+
when :producer then ::OpenTelemetry::Trace::SpanKind::PRODUCER
|
|
126
|
+
when :consumer then ::OpenTelemetry::Trace::SpanKind::CONSUMER
|
|
127
|
+
else ::OpenTelemetry::Trace::SpanKind::INTERNAL
|
|
128
|
+
end
|
|
129
|
+
rescue StandardError
|
|
130
|
+
::OpenTelemetry::Trace::SpanKind::INTERNAL
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def time_to_nano(time)
|
|
134
|
+
return (Time.now.to_f * 1_000_000_000).to_i if time.nil?
|
|
135
|
+
|
|
136
|
+
time = Time.parse(time.to_s) if time.is_a?(String)
|
|
137
|
+
(time.to_f * 1_000_000_000).to_i
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
data/lib/e11y/pii/patterns.rb
CHANGED
|
@@ -15,7 +15,7 @@ module E11y
|
|
|
15
15
|
EMAIL = /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/
|
|
16
16
|
|
|
17
17
|
# Password-like field names
|
|
18
|
-
PASSWORD_FIELDS =
|
|
18
|
+
PASSWORD_FIELDS = /\b(?:password|passwd|pwd|secret|token|api[_-]?key)\b/i
|
|
19
19
|
|
|
20
20
|
# Social Security Number (US format: XXX-XX-XXXX)
|
|
21
21
|
SSN = /\b\d{3}-\d{2}-\d{4}\b/
|
|
@@ -40,6 +40,17 @@ module E11y
|
|
|
40
40
|
PHONE
|
|
41
41
|
].freeze
|
|
42
42
|
|
|
43
|
+
# Patterns applied to STRING VALUES only (excludes PASSWORD_FIELDS).
|
|
44
|
+
# PASSWORD_FIELDS matches field names (password, token, api_key), not values.
|
|
45
|
+
# Applying it to values corrupts legitimate strings like "process_token_renewal_completed".
|
|
46
|
+
VALUE_PATTERNS = [
|
|
47
|
+
EMAIL,
|
|
48
|
+
SSN,
|
|
49
|
+
CREDIT_CARD,
|
|
50
|
+
IPV4,
|
|
51
|
+
PHONE
|
|
52
|
+
].freeze
|
|
53
|
+
|
|
43
54
|
# Field name patterns that indicate PII
|
|
44
55
|
# Used for field-level detection (case-insensitive)
|
|
45
56
|
FIELD_PATTERNS = {
|
|
@@ -12,7 +12,7 @@ module E11y
|
|
|
12
12
|
#
|
|
13
13
|
# builder.use E11y::Middleware::TraceContext
|
|
14
14
|
# builder.use E11y::Middleware::Validation
|
|
15
|
-
# builder.use E11y::Middleware::
|
|
15
|
+
# builder.use E11y::Middleware::PIIFilter
|
|
16
16
|
#
|
|
17
17
|
# builder.validate_zones! # Boot-time validation
|
|
18
18
|
#
|
|
@@ -26,14 +26,14 @@ module E11y
|
|
|
26
26
|
# end
|
|
27
27
|
#
|
|
28
28
|
# builder.zone(:security) do
|
|
29
|
-
# use E11y::Middleware::
|
|
29
|
+
# use E11y::Middleware::PIIFilter
|
|
30
30
|
# end
|
|
31
31
|
#
|
|
32
32
|
# @see E11y::Middleware::Base
|
|
33
33
|
# @see ADR-015 §3.4 Middleware Zones & Modification Rules
|
|
34
34
|
class Builder
|
|
35
35
|
# Middleware entry: [middleware_class, args, options]
|
|
36
|
-
MiddlewareEntry = Struct.new(:middleware_class, :args, :options
|
|
36
|
+
MiddlewareEntry = Struct.new(:middleware_class, :args, :options)
|
|
37
37
|
|
|
38
38
|
# @return [Array<MiddlewareEntry>] Registered middlewares
|
|
39
39
|
attr_reader :middlewares
|
|
@@ -80,7 +80,7 @@ module E11y
|
|
|
80
80
|
#
|
|
81
81
|
# @example
|
|
82
82
|
# builder.zone(:security) do
|
|
83
|
-
# use E11y::Middleware::
|
|
83
|
+
# use E11y::Middleware::PIIFilter
|
|
84
84
|
# end
|
|
85
85
|
#
|
|
86
86
|
# @see ADR-015 §3.4.2 Middleware Zones
|
|
@@ -38,11 +38,13 @@ module E11y
|
|
|
38
38
|
module AuditEvent
|
|
39
39
|
def self.included(base)
|
|
40
40
|
base.class_eval do
|
|
41
|
-
|
|
41
|
+
audit_event true
|
|
42
|
+
contains_pii false # Preserve all data for signing (Tier 1 = skip filtering)
|
|
43
|
+
use_dlq true # Audit events always saved to DLQ (compliance)
|
|
42
44
|
# Severity is NOT set by preset - user decides based on event criticality
|
|
43
45
|
end
|
|
44
46
|
|
|
45
|
-
# Extend class with audit-specific methods
|
|
47
|
+
# Extend class with audit-specific methods (resolve_sample_rate 1.0, resolve_rate_limit nil)
|
|
46
48
|
base.extend(ClassMethods)
|
|
47
49
|
end
|
|
48
50
|
|
|
@@ -59,6 +61,15 @@ module E11y
|
|
|
59
61
|
def resolve_sample_rate
|
|
60
62
|
1.0 # 100% - compliance requirement
|
|
61
63
|
end
|
|
64
|
+
|
|
65
|
+
# Audit events use routing rules (UC-012), not severity-based adapters.
|
|
66
|
+
# Return [] when no explicit adapters; respect explicit adapters when set.
|
|
67
|
+
def adapters(*list)
|
|
68
|
+
@adapters = list.flatten if list.any?
|
|
69
|
+
return @adapters if @adapters
|
|
70
|
+
|
|
71
|
+
[]
|
|
72
|
+
end
|
|
62
73
|
end
|
|
63
74
|
end
|
|
64
75
|
end
|
data/lib/e11y/railtie.rb
CHANGED
|
@@ -22,7 +22,7 @@ module E11y
|
|
|
22
22
|
# # config/initializers/e11y.rb
|
|
23
23
|
# E11y.configure do |config|
|
|
24
24
|
# config.service_name = "my-app"
|
|
25
|
-
# config.adapters
|
|
25
|
+
# config.adapters[:loki] = E11y::Adapters::Loki.new(url: ENV['LOKI_URL'])
|
|
26
26
|
# end
|
|
27
27
|
#
|
|
28
28
|
# @see ADR-008 §3 (Railtie & Initialization)
|
|
@@ -33,6 +33,13 @@ module E11y
|
|
|
33
33
|
# including configuration, middleware insertion, instrumentation setup,
|
|
34
34
|
# and console integration.
|
|
35
35
|
class Railtie < Rails::Railtie
|
|
36
|
+
# Wire up generators so `rails g e11y:*` commands are discoverable.
|
|
37
|
+
generators do
|
|
38
|
+
require "generators/e11y/install/install_generator"
|
|
39
|
+
require "generators/e11y/event/event_generator"
|
|
40
|
+
require "generators/e11y/grafana_dashboard/grafana_dashboard_generator"
|
|
41
|
+
require "generators/e11y/prometheus_alerts/prometheus_alerts_generator"
|
|
42
|
+
end
|
|
36
43
|
# Derive service name from Rails application class
|
|
37
44
|
# @return [String] Service name (e.g., "my_app")
|
|
38
45
|
def self.derive_service_name
|
|
@@ -47,7 +54,8 @@ module E11y
|
|
|
47
54
|
E11y.configure do |config|
|
|
48
55
|
config.environment ||= Rails.env.to_s
|
|
49
56
|
config.service_name ||= E11y::Railtie.derive_service_name
|
|
50
|
-
#
|
|
57
|
+
# Enable in dev/prod; disable in test by default — only when still unset (nil).
|
|
58
|
+
# Respects explicit true/false from earlier E11y.configure (e.g. config/application.rb).
|
|
51
59
|
config.enabled = !Rails.env.test? if config.enabled.nil?
|
|
52
60
|
end
|
|
53
61
|
end
|
|
@@ -57,10 +65,37 @@ module E11y
|
|
|
57
65
|
next unless E11y.config.enabled
|
|
58
66
|
|
|
59
67
|
# Setup instruments (each can be enabled/disabled separately)
|
|
60
|
-
E11y::Railtie.setup_rails_instrumentation if E11y.config.
|
|
61
|
-
E11y::Railtie.setup_logger_bridge if E11y.config.
|
|
62
|
-
E11y::Railtie.setup_sidekiq if defined?(::Sidekiq) && E11y.config.
|
|
63
|
-
E11y::Railtie.setup_active_job if defined?(::ActiveJob) && E11y.config.
|
|
68
|
+
E11y::Railtie.setup_rails_instrumentation if E11y.config.rails_instrumentation_enabled
|
|
69
|
+
E11y::Railtie.setup_logger_bridge if E11y.config.logger_bridge_enabled
|
|
70
|
+
E11y::Railtie.setup_sidekiq if defined?(::Sidekiq) && E11y.config.sidekiq_enabled
|
|
71
|
+
E11y::Railtie.setup_active_job if defined?(::ActiveJob) && E11y.config.active_job_enabled
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Outgoing HTTP trace propagation (UC-009)
|
|
75
|
+
initializer "e11y.http_tracing", after: :load_config_initializers do
|
|
76
|
+
next unless E11y.configuration.enable_http_tracing
|
|
77
|
+
|
|
78
|
+
E11y::Tracing.patch_net_http!
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Auto-register DevLog adapter in development and test environments.
|
|
82
|
+
# Skipped if the user has already registered :dev_log in their initializer.
|
|
83
|
+
initializer "e11y.setup_development", after: :load_config_initializers do |app|
|
|
84
|
+
next unless Rails.env.development? || Rails.env.test?
|
|
85
|
+
next if E11y.configuration.adapters.key?(:dev_log)
|
|
86
|
+
|
|
87
|
+
E11y.configure do |config|
|
|
88
|
+
config.register_adapter :dev_log, E11y::Adapters::DevLog.new(
|
|
89
|
+
path: Rails.root.join("log", "e11y_dev.jsonl"),
|
|
90
|
+
max_lines: ENV.fetch("E11Y_MAX_EVENTS", "10000").to_i,
|
|
91
|
+
max_size: ENV.fetch("E11Y_MAX_SIZE", "50").to_i * 1024 * 1024,
|
|
92
|
+
keep_rotated: ENV.fetch("E11Y_KEEP_ROTATED", "5").to_i,
|
|
93
|
+
enable_watcher: !Rails.env.test?
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
require "e11y/middleware/dev_log_source"
|
|
98
|
+
app.middleware.use E11y::Middleware::DevLogSource
|
|
64
99
|
end
|
|
65
100
|
|
|
66
101
|
# Middleware insertion
|
|
@@ -69,10 +104,13 @@ module E11y
|
|
|
69
104
|
|
|
70
105
|
# Insert E11y request middleware before Rails logger
|
|
71
106
|
# This ensures trace context is set up before any Rails logging
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
E11y::Middleware::Request
|
|
75
|
-
|
|
107
|
+
# API-only mode may omit Rails::Rack::Logger — fall back to unshift
|
|
108
|
+
begin
|
|
109
|
+
app.middleware.insert_before(Rails::Rack::Logger, E11y::Middleware::Request)
|
|
110
|
+
rescue RuntimeError
|
|
111
|
+
# Rails::Rack::Logger not in stack (e.g. api_only)
|
|
112
|
+
app.middleware.unshift(E11y::Middleware::Request)
|
|
113
|
+
end
|
|
76
114
|
end
|
|
77
115
|
|
|
78
116
|
# Console helpers
|
|
@@ -87,10 +125,10 @@ module E11y
|
|
|
87
125
|
|
|
88
126
|
# Rake task helpers
|
|
89
127
|
rake_tasks do
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
128
|
+
load File.expand_path("../tasks/e11y_slo.rake", __dir__)
|
|
129
|
+
load File.expand_path("../tasks/e11y_lint.rake", __dir__)
|
|
130
|
+
load File.expand_path("../tasks/e11y_events.rake", __dir__)
|
|
131
|
+
load File.expand_path("../tasks/e11y_docs.rake", __dir__)
|
|
94
132
|
end
|
|
95
133
|
|
|
96
134
|
# Setup Rails instrumentation (ActiveSupport::Notifications → E11y)
|