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,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
|
data/lib/e11y/logger/bridge.rb
CHANGED
|
@@ -8,7 +8,7 @@ module E11y
|
|
|
8
8
|
#
|
|
9
9
|
# Transparent wrapper around Rails.logger that:
|
|
10
10
|
# 1. Delegates all calls to the original logger (preserves Rails behavior)
|
|
11
|
-
# 2. Tracks log calls as E11y events (when
|
|
11
|
+
# 2. Tracks log calls as E11y events (when logger_bridge_enabled = true)
|
|
12
12
|
#
|
|
13
13
|
# **Why SimpleDelegator instead of full replacement:**
|
|
14
14
|
# - ✅ Simpler: No need to reimplement entire Logger API
|
|
@@ -17,12 +17,12 @@ module E11y
|
|
|
17
17
|
# - ✅ Rails Way: Extends functionality without replacing core components
|
|
18
18
|
#
|
|
19
19
|
# @example Basic usage
|
|
20
|
-
# # Automatically enabled by E11y::Railtie if config.
|
|
20
|
+
# # Automatically enabled by E11y::Railtie if config.logger_bridge_enabled = true
|
|
21
21
|
# Rails.logger = E11y::Logger::Bridge.new(Rails.logger)
|
|
22
22
|
#
|
|
23
23
|
# @example Manual setup
|
|
24
24
|
# E11y.configure do |config|
|
|
25
|
-
# config.
|
|
25
|
+
# config.logger_bridge_enabled = true # Wrap Rails.logger and send logs to E11y
|
|
26
26
|
# end
|
|
27
27
|
#
|
|
28
28
|
# @see ADR-008 §7 (Rails.logger Migration)
|
|
@@ -34,7 +34,7 @@ module E11y
|
|
|
34
34
|
#
|
|
35
35
|
# @return [void]
|
|
36
36
|
def self.setup!
|
|
37
|
-
return unless E11y.config.
|
|
37
|
+
return unless E11y.config.logger_bridge_enabled
|
|
38
38
|
return unless defined?(::Rails)
|
|
39
39
|
|
|
40
40
|
# Wrap Rails.logger (preserves original behavior)
|
|
@@ -53,6 +53,8 @@ module E11y
|
|
|
53
53
|
::Logger::FATAL => :fatal,
|
|
54
54
|
::Logger::UNKNOWN => :warn
|
|
55
55
|
}
|
|
56
|
+
@track_severities_set = build_track_severities_set(E11y.config.logger_bridge_track_severities)
|
|
57
|
+
@ignore_patterns = build_compiled_patterns(E11y.config.logger_bridge_ignore_patterns)
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
# Intercept logger methods to track to E11y
|
|
@@ -124,18 +126,22 @@ module E11y
|
|
|
124
126
|
# @param message [String, nil] Log message
|
|
125
127
|
# @yield Block that returns log message
|
|
126
128
|
# @return [void]
|
|
127
|
-
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
128
129
|
# Logger tracking requires message extraction, validation, event class lookup, and error handling
|
|
129
130
|
def track_to_e11y(severity, message = nil)
|
|
130
131
|
# Extract message
|
|
131
132
|
msg = message || (block_given? ? yield : nil)
|
|
132
133
|
return if msg.nil? || (msg.respond_to?(:empty?) && msg.empty?)
|
|
133
134
|
|
|
135
|
+
msg_str = msg.to_s
|
|
136
|
+
|
|
137
|
+
return if @track_severities_set && !@track_severities_set.include?(severity)
|
|
138
|
+
return if @ignore_patterns.any? { |re| re.match?(msg_str) }
|
|
139
|
+
|
|
134
140
|
# Track to E11y using severity-specific class
|
|
135
141
|
require "e11y/events/rails/log"
|
|
136
142
|
event_class = event_class_for_severity(severity)
|
|
137
143
|
event_class.track(
|
|
138
|
-
message:
|
|
144
|
+
message: msg_str,
|
|
139
145
|
caller_location: extract_caller_location
|
|
140
146
|
)
|
|
141
147
|
rescue StandardError => e
|
|
@@ -143,7 +149,6 @@ module E11y
|
|
|
143
149
|
# In development/test, you might want to log this
|
|
144
150
|
warn "E11y logger tracking failed: #{e.message}" if defined?(::Rails) && ::Rails.env.development?
|
|
145
151
|
end
|
|
146
|
-
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
147
152
|
|
|
148
153
|
# Get event class for severity
|
|
149
154
|
# @param severity [Symbol] E11y severity
|
|
@@ -162,6 +167,20 @@ module E11y
|
|
|
162
167
|
end
|
|
163
168
|
# rubocop:enable Lint/DuplicateBranch
|
|
164
169
|
|
|
170
|
+
def build_track_severities_set(severities)
|
|
171
|
+
return nil if severities.nil? || (severities.respond_to?(:empty?) && severities.empty?)
|
|
172
|
+
|
|
173
|
+
Set.new(Array(severities).map(&:to_sym))
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def build_compiled_patterns(patterns)
|
|
177
|
+
return [] if patterns.nil? || !patterns.respond_to?(:any?) || patterns.none?
|
|
178
|
+
|
|
179
|
+
Array(patterns).map do |p|
|
|
180
|
+
p.is_a?(Regexp) ? p : Regexp.new(Regexp.escape(p.to_s))
|
|
181
|
+
end.freeze
|
|
182
|
+
end
|
|
183
|
+
|
|
165
184
|
# Extract caller location (first caller outside E11y)
|
|
166
185
|
# @return [String, nil] Caller location string
|
|
167
186
|
def extract_caller_location
|
|
@@ -7,11 +7,10 @@ module E11y
|
|
|
7
7
|
module Metrics
|
|
8
8
|
# Cardinality protection for metrics labels.
|
|
9
9
|
#
|
|
10
|
-
# Implements
|
|
10
|
+
# Implements 3-layer defense system to prevent cardinality explosions:
|
|
11
11
|
# 1. Universal Denylist - Block high-cardinality fields (user_id, order_id, etc.)
|
|
12
12
|
# 2. Per-Metric Limits - Track unique values per metric, drop if exceeded
|
|
13
|
-
# 3. Dynamic
|
|
14
|
-
# 4. Dynamic Actions - Auto-relabeling, alerting, or dropping on overflow
|
|
13
|
+
# 3. Dynamic Actions - Drop, alert, or relabel on overflow
|
|
15
14
|
#
|
|
16
15
|
# Now supports optional relabeling to reduce cardinality while preserving signal.
|
|
17
16
|
#
|
|
@@ -37,7 +36,7 @@ module E11y
|
|
|
37
36
|
# @see ADR-002 §4 (Cardinality Protection)
|
|
38
37
|
# @see UC-013 (High Cardinality Protection)
|
|
39
38
|
# rubocop:disable Metrics/ClassLength
|
|
40
|
-
# Cardinality protection is a cohesive
|
|
39
|
+
# Cardinality protection is a cohesive 3-layer defense system against metric explosions
|
|
41
40
|
class CardinalityProtection
|
|
42
41
|
# Universal denylist - high-cardinality fields that should NEVER be labels
|
|
43
42
|
UNIVERSAL_DENYLIST = %i[
|
|
@@ -64,7 +63,7 @@ module E11y
|
|
|
64
63
|
# Default per-metric cardinality limit
|
|
65
64
|
DEFAULT_CARDINALITY_LIMIT = 1000
|
|
66
65
|
|
|
67
|
-
# Overflow strategies (Layer
|
|
66
|
+
# Overflow strategies (Layer 3: Dynamic Actions)
|
|
68
67
|
OVERFLOW_STRATEGIES = %i[drop alert relabel].freeze
|
|
69
68
|
|
|
70
69
|
# Default overflow strategy
|
|
@@ -85,7 +84,6 @@ module E11y
|
|
|
85
84
|
# @option config [Float] :alert_threshold (0.8) Alert when cardinality reaches this ratio
|
|
86
85
|
# @option config [Proc] :alert_callback Optional callback when alert triggered
|
|
87
86
|
# @option config [Boolean] :auto_relabel (false) Auto-relabel to [OTHER] on overflow
|
|
88
|
-
# rubocop:disable Metrics/AbcSize
|
|
89
87
|
# Cardinality protection initialization requires extracting multiple config options and setting up components
|
|
90
88
|
def initialize(config = {})
|
|
91
89
|
@cardinality_limit = config.fetch(:cardinality_limit, DEFAULT_CARDINALITY_LIMIT)
|
|
@@ -109,7 +107,6 @@ module E11y
|
|
|
109
107
|
@overflow_counts = Hash.new(0)
|
|
110
108
|
@overflow_mutex = Mutex.new
|
|
111
109
|
end
|
|
112
|
-
# rubocop:enable Metrics/AbcSize
|
|
113
110
|
|
|
114
111
|
# Define relabeling rule for a label
|
|
115
112
|
#
|
|
@@ -222,7 +219,6 @@ module E11y
|
|
|
222
219
|
|
|
223
220
|
# Check if approaching alert threshold (Layer 3: Monitoring)
|
|
224
221
|
# @param metric_name [String] Metric name
|
|
225
|
-
# rubocop:disable Metrics/MethodLength
|
|
226
222
|
# Alert threshold checking requires calculating ratio, checking conditions, and sending detailed alerts
|
|
227
223
|
def check_alert_threshold(metric_name)
|
|
228
224
|
return unless @alert_threshold
|
|
@@ -252,7 +248,6 @@ module E11y
|
|
|
252
248
|
# Track metric
|
|
253
249
|
track_cardinality_metric(metric_name, :threshold_exceeded, current_cardinality)
|
|
254
250
|
end
|
|
255
|
-
# rubocop:enable Metrics/MethodLength
|
|
256
251
|
|
|
257
252
|
# Handle overflow when cardinality limit exceeded (Layer 4: Dynamic Actions)
|
|
258
253
|
# @param metric_name [String] Metric name
|
|
@@ -311,9 +306,11 @@ module E11y
|
|
|
311
306
|
severity: :error
|
|
312
307
|
)
|
|
313
308
|
|
|
314
|
-
# Also log warning
|
|
315
|
-
warn
|
|
316
|
-
|
|
309
|
+
# Also log warning (via E11y.logger so it respects Rails.logger in test env)
|
|
310
|
+
E11y.logger&.warn(
|
|
311
|
+
"E11y Metrics: Cardinality limit exceeded for #{metric_name}:#{key} " \
|
|
312
|
+
"(limit: #{@cardinality_limit}, current: #{current_cardinality})"
|
|
313
|
+
)
|
|
317
314
|
end
|
|
318
315
|
|
|
319
316
|
# Handle relabel strategy - relabel to [OTHER]
|
|
@@ -385,7 +382,6 @@ module E11y
|
|
|
385
382
|
# @param metric_name [String] Metric name
|
|
386
383
|
# @param action [Symbol] Action type (:threshold_exceeded, :drop, :alert, :relabel)
|
|
387
384
|
# @param value [Integer] Metric value
|
|
388
|
-
# rubocop:disable Metrics/MethodLength
|
|
389
385
|
# Cardinality tracking requires incrementing overflow counters and updating gauge metrics
|
|
390
386
|
def track_cardinality_metric(metric_name, action, value)
|
|
391
387
|
return unless defined?(E11y::Metrics)
|
|
@@ -408,9 +404,8 @@ module E11y
|
|
|
408
404
|
)
|
|
409
405
|
rescue StandardError => e
|
|
410
406
|
# Don't fail on metrics tracking errors
|
|
411
|
-
warn
|
|
407
|
+
E11y.logger&.warn("E11y: Failed to track cardinality metric: #{e.message}")
|
|
412
408
|
end
|
|
413
|
-
# rubocop:enable Metrics/MethodLength
|
|
414
409
|
end
|
|
415
410
|
# rubocop:enable Metrics/ClassLength
|
|
416
411
|
end
|
|
@@ -33,13 +33,15 @@ module E11y
|
|
|
33
33
|
# Records unique label values per metric+label combination.
|
|
34
34
|
# Thread-safe operation.
|
|
35
35
|
#
|
|
36
|
-
# @param metric_name [String] Metric name
|
|
36
|
+
# @param metric_name [String, Symbol] Metric name
|
|
37
37
|
# @param label_key [Symbol, String] Label key
|
|
38
38
|
# @param label_value [Object] Label value to track
|
|
39
39
|
# @return [Boolean] true if within limit, false if limit exceeded
|
|
40
40
|
def track(metric_name, label_key, label_value)
|
|
41
41
|
@mutex.synchronize do
|
|
42
|
-
|
|
42
|
+
# Normalize metric_name to string for consistent key access
|
|
43
|
+
metric_key = metric_name.to_s
|
|
44
|
+
value_set = @tracker[metric_key][label_key]
|
|
43
45
|
|
|
44
46
|
# Allow if already tracked (existing value)
|
|
45
47
|
return true if value_set.include?(label_value)
|
|
@@ -66,7 +68,9 @@ module E11y
|
|
|
66
68
|
# @return [void]
|
|
67
69
|
def force_track(metric_name, label_key, label_value)
|
|
68
70
|
@mutex.synchronize do
|
|
69
|
-
|
|
71
|
+
# Normalize metric_name to string for consistent key access
|
|
72
|
+
metric_key = metric_name.to_s
|
|
73
|
+
value_set = @tracker[metric_key][label_key]
|
|
70
74
|
value_set.add(label_value) unless value_set.include?(label_value)
|
|
71
75
|
end
|
|
72
76
|
end
|
|
@@ -78,7 +82,9 @@ module E11y
|
|
|
78
82
|
# @return [Boolean] true if at or above limit
|
|
79
83
|
def exceeded?(metric_name, label_key)
|
|
80
84
|
@mutex.synchronize do
|
|
81
|
-
|
|
85
|
+
# Normalize metric_name to string for consistent key access
|
|
86
|
+
metric_key = metric_name.to_s
|
|
87
|
+
@tracker.dig(metric_key, label_key)&.size.to_i >= @limit
|
|
82
88
|
end
|
|
83
89
|
end
|
|
84
90
|
|
|
@@ -89,7 +95,9 @@ module E11y
|
|
|
89
95
|
# @return [Integer] Number of unique values tracked
|
|
90
96
|
def cardinality(metric_name, label_key)
|
|
91
97
|
@mutex.synchronize do
|
|
92
|
-
|
|
98
|
+
# Normalize metric_name to string for consistent key access
|
|
99
|
+
metric_key = metric_name.to_s
|
|
100
|
+
@tracker.dig(metric_key, label_key)&.size || 0
|
|
93
101
|
end
|
|
94
102
|
end
|
|
95
103
|
|
|
@@ -99,7 +107,9 @@ module E11y
|
|
|
99
107
|
# @return [Hash{Symbol => Integer}] Label key => cardinality
|
|
100
108
|
def cardinalities(metric_name)
|
|
101
109
|
@mutex.synchronize do
|
|
102
|
-
|
|
110
|
+
# Normalize metric_name to string for consistent key access
|
|
111
|
+
metric_key = metric_name.to_s
|
|
112
|
+
metric_data = @tracker[metric_key]
|
|
103
113
|
metric_data.transform_values(&:size)
|
|
104
114
|
end
|
|
105
115
|
end
|
|
@@ -6,7 +6,7 @@ module E11y
|
|
|
6
6
|
module Metrics
|
|
7
7
|
# Registry for metric configurations.
|
|
8
8
|
#
|
|
9
|
-
# Stores metric definitions and provides
|
|
9
|
+
# Stores metric definitions and provides event-name matching.
|
|
10
10
|
# This is a singleton class - use Registry.instance to access it.
|
|
11
11
|
# All metrics (global, event-level, preset) are registered here for validation.
|
|
12
12
|
#
|
|
@@ -144,7 +144,6 @@ module E11y
|
|
|
144
144
|
# Validate metric configuration
|
|
145
145
|
# @param config [Hash] Metric configuration
|
|
146
146
|
# @raise [ArgumentError] if configuration is invalid
|
|
147
|
-
# rubocop:disable Metrics/AbcSize
|
|
148
147
|
def validate_config!(config)
|
|
149
148
|
raise ArgumentError, "Metric type is required" unless config[:type]
|
|
150
149
|
raise ArgumentError, "Invalid metric type: #{config[:type]}" unless %i[counter histogram
|
|
@@ -156,14 +155,13 @@ module E11y
|
|
|
156
155
|
|
|
157
156
|
raise ArgumentError, "Value extractor is required for #{config[:type]} metrics"
|
|
158
157
|
end
|
|
159
|
-
# rubocop:enable Metrics/AbcSize
|
|
160
158
|
|
|
161
159
|
# Validate that new metric doesn't conflict with existing one
|
|
162
160
|
# @param existing [Hash] Existing metric configuration
|
|
163
161
|
# @param new_config [Hash] New metric configuration
|
|
164
162
|
# @raise [TypeConflictError] if types don't match
|
|
165
163
|
# @raise [LabelConflictError] if labels don't match
|
|
166
|
-
# rubocop:disable Metrics/AbcSize, Metrics/
|
|
164
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
167
165
|
# Conflict validation requires checking type and labels with detailed error messages
|
|
168
166
|
def validate_no_conflicts!(existing, new_config)
|
|
169
167
|
# Check 1: Type must match
|
|
@@ -215,7 +213,7 @@ module E11y
|
|
|
215
213
|
Using existing buckets.
|
|
216
214
|
WARNING
|
|
217
215
|
end
|
|
218
|
-
# rubocop:enable Metrics/AbcSize, Metrics/
|
|
216
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
219
217
|
|
|
220
218
|
# Compile glob pattern to regex
|
|
221
219
|
# @param pattern [String] Glob pattern (e.g., "order.*", "user.*.created")
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Metrics
|
|
5
|
+
# In-memory metrics backend for tests.
|
|
6
|
+
#
|
|
7
|
+
# Records all metric calls so test assertions can verify what was tracked
|
|
8
|
+
# without using mocks on E11y::Metrics directly.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# backend = E11y::Metrics::TestBackend.new
|
|
12
|
+
# E11y::Metrics.instance_variable_set(:@backend, backend)
|
|
13
|
+
#
|
|
14
|
+
# MyService.call
|
|
15
|
+
#
|
|
16
|
+
# expect(backend.increment_count(:orders_total)).to eq(1)
|
|
17
|
+
# expect(backend.increments).to include(hash_including(name: :orders_total))
|
|
18
|
+
class TestBackend
|
|
19
|
+
attr_reader :increments, :histograms, :gauges
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
reset!
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param name [Symbol] Metric name
|
|
26
|
+
# @param labels [Hash] Metric labels
|
|
27
|
+
# @param value [Integer] Increment amount
|
|
28
|
+
def increment(name, labels = {}, value: 1)
|
|
29
|
+
@increments << { name: name, labels: labels, value: value }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @param name [Symbol] Metric name
|
|
33
|
+
# @param value [Numeric] Observed value
|
|
34
|
+
# @param labels [Hash] Metric labels
|
|
35
|
+
def histogram(name, value, labels = {}, buckets: nil) # rubocop:todo Lint/UnusedMethodArgument
|
|
36
|
+
@histograms << { name: name, value: value, labels: labels }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param name [Symbol] Metric name
|
|
40
|
+
# @param value [Numeric] Gauge value
|
|
41
|
+
# @param labels [Hash] Metric labels
|
|
42
|
+
def gauge(name, value, labels = {})
|
|
43
|
+
@gauges << { name: name, value: value, labels: labels }
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Reset all recorded metrics.
|
|
47
|
+
def reset!
|
|
48
|
+
@increments = []
|
|
49
|
+
@histograms = []
|
|
50
|
+
@gauges = []
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Count how many times a counter was incremented (any labels).
|
|
54
|
+
#
|
|
55
|
+
# @param name [Symbol] Metric name
|
|
56
|
+
# @return [Integer]
|
|
57
|
+
def increment_count(name)
|
|
58
|
+
@increments.count { |r| r[:name] == name }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
data/lib/e11y/metrics.rb
CHANGED
|
@@ -21,18 +21,41 @@ module E11y
|
|
|
21
21
|
# @see ADR-002 §3 (Metrics Integration)
|
|
22
22
|
# @see ADR-016 §3 (Self-Monitoring Metrics)
|
|
23
23
|
module Metrics
|
|
24
|
+
# No-op metrics backend used when no real backend (e.g. Yabeda) is configured.
|
|
25
|
+
# Accepts all metric calls and silently discards them so callers never
|
|
26
|
+
# need to guard against a nil backend.
|
|
27
|
+
class NullBackend
|
|
28
|
+
def increment(_name, _labels = {}, value: 1); end
|
|
29
|
+
def histogram(_name, _value, _labels = {}, buckets: nil); end
|
|
30
|
+
def gauge(_name, _value, _labels = {}); end
|
|
31
|
+
end
|
|
32
|
+
|
|
24
33
|
class << self
|
|
25
34
|
# Track a counter metric (monotonically increasing value).
|
|
26
35
|
#
|
|
27
|
-
#
|
|
36
|
+
# Accepts dotted names (e.g., "e11y.ephemeral_buffer.flushed") and normalizes to
|
|
37
|
+
# underscores. DLQ metrics get _total suffix. Labels[:events] is used as value if present.
|
|
38
|
+
# Safe: no-op when backend unavailable, rescues errors.
|
|
39
|
+
#
|
|
40
|
+
# @param name [Symbol, String] Metric name (e.g., :http_requests_total or "e11y.ephemeral_buffer.flushed")
|
|
28
41
|
# @param labels [Hash] Metric labels (e.g., { method: 'GET', status: 200 })
|
|
29
|
-
# @param value [Integer] Increment value (default: 1)
|
|
42
|
+
# @param value [Integer] Increment value (default: 1, overridden by labels[:events] if present)
|
|
30
43
|
# @return [void]
|
|
31
44
|
#
|
|
32
45
|
# @example
|
|
33
|
-
# E11y::Metrics.increment(:e11y_events_tracked,
|
|
34
|
-
|
|
35
|
-
|
|
46
|
+
# E11y::Metrics.increment(:e11y_events_tracked, event_type: 'order.created')
|
|
47
|
+
# E11y::Metrics.increment("e11y.ephemeral_buffer.flushed_on_error", value: 5)
|
|
48
|
+
def increment(name, labels = {}, value: 1, **labels_kw)
|
|
49
|
+
return unless backend
|
|
50
|
+
|
|
51
|
+
labels = labels.merge(labels_kw) unless labels_kw.empty?
|
|
52
|
+
value = labels.delete(:events) if labels.key?(:events)
|
|
53
|
+
value ||= 1
|
|
54
|
+
|
|
55
|
+
normalized = normalized_metric_name(name)
|
|
56
|
+
backend.increment(normalized, labels, value: value)
|
|
57
|
+
rescue StandardError => e
|
|
58
|
+
E11y.logger&.debug("E11y metrics: #{e.message}")
|
|
36
59
|
end
|
|
37
60
|
|
|
38
61
|
# Track a histogram metric (distribution of values).
|
|
@@ -44,9 +67,15 @@ module E11y
|
|
|
44
67
|
# @return [void]
|
|
45
68
|
#
|
|
46
69
|
# @example
|
|
47
|
-
# E11y::Metrics.histogram(:e11y_track_duration_seconds, 0.0005,
|
|
48
|
-
def histogram(name, value, labels = {}, buckets: nil)
|
|
49
|
-
|
|
70
|
+
# E11y::Metrics.histogram(:e11y_track_duration_seconds, 0.0005, event_type: 'order.created')
|
|
71
|
+
def histogram(name, value, labels = {}, buckets: nil, **labels_kw)
|
|
72
|
+
return unless backend
|
|
73
|
+
|
|
74
|
+
labels = labels.merge(labels_kw) unless labels_kw.empty?
|
|
75
|
+
normalized = normalized_metric_name(name)
|
|
76
|
+
backend.histogram(normalized, value, labels, buckets: buckets)
|
|
77
|
+
rescue StandardError => e
|
|
78
|
+
E11y.logger&.debug("E11y metrics: #{e.message}")
|
|
50
79
|
end
|
|
51
80
|
|
|
52
81
|
# Track a gauge metric (current value that can go up or down).
|
|
@@ -81,10 +110,27 @@ module E11y
|
|
|
81
110
|
# @api private
|
|
82
111
|
def reset_backend!
|
|
83
112
|
remove_instance_variable(:@backend) if defined?(@backend)
|
|
113
|
+
@name_cache = nil if defined?(@name_cache)
|
|
84
114
|
end
|
|
85
115
|
|
|
86
116
|
private
|
|
87
117
|
|
|
118
|
+
# Normalize metric name: dots to underscores, DLQ metrics get _total suffix.
|
|
119
|
+
# Cached to avoid repeated string allocations for hot-path metrics.
|
|
120
|
+
#
|
|
121
|
+
# @param name [Symbol, String] Raw metric name
|
|
122
|
+
# @return [Symbol] Normalized name for Prometheus (e.g., e11y_ephemeral_buffer_flushed_on_error)
|
|
123
|
+
def normalized_metric_name(name)
|
|
124
|
+
@name_cache ||= {}
|
|
125
|
+
@name_cache[name] ||= compute_normalized_name(name)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def compute_normalized_name(name)
|
|
129
|
+
s = name.to_s.tr(".", "_")
|
|
130
|
+
s = "#{s}_total" if s.include?("e11y_dlq_") && !s.end_with?("_total")
|
|
131
|
+
s.to_sym
|
|
132
|
+
end
|
|
133
|
+
|
|
88
134
|
# Detect the metrics backend from configured adapters.
|
|
89
135
|
#
|
|
90
136
|
# @return [Object, nil] Metrics backend or nil
|
|
@@ -99,8 +145,8 @@ module E11y
|
|
|
99
145
|
# rubocop:enable Style/ClassEqualityComparison
|
|
100
146
|
return yabeda_adapter if yabeda_adapter
|
|
101
147
|
|
|
102
|
-
# No
|
|
103
|
-
|
|
148
|
+
# No Yabeda adapter configured — fall back to NullBackend
|
|
149
|
+
NullBackend.new
|
|
104
150
|
end
|
|
105
151
|
end
|
|
106
152
|
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Middleware
|
|
5
|
+
# Resolves target adapter names for an event (shared by PIIFilter and Routing).
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module AdapterResolver
|
|
9
|
+
# Resolve target adapters for event_data (explicit or routing rules).
|
|
10
|
+
#
|
|
11
|
+
# @param event_data [Hash] Event data with :adapters, :audit_event, :retention_until, etc.
|
|
12
|
+
# @return [Array<Symbol>] Target adapter names
|
|
13
|
+
def self.resolve(event_data)
|
|
14
|
+
if event_data[:adapters]&.any?
|
|
15
|
+
Array(event_data[:adapters]).map(&:to_sym)
|
|
16
|
+
else
|
|
17
|
+
apply_routing_rules(event_data)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.apply_routing_rules(event_data)
|
|
22
|
+
matched_adapters = []
|
|
23
|
+
rules = E11y.configuration.routing_rules || []
|
|
24
|
+
|
|
25
|
+
rules.each do |rule|
|
|
26
|
+
result = rule.call(event_data)
|
|
27
|
+
matched_adapters.concat(Array(result)) if result
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
warn "[E11y] Routing rule error: #{e.message}"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
if matched_adapters.any?
|
|
33
|
+
matched_adapters.uniq
|
|
34
|
+
else
|
|
35
|
+
E11y.configuration.fallback_adapters || [:stdout]
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|