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,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
|
|
6
|
+
module E11y
|
|
7
|
+
module Adapters
|
|
8
|
+
# Development-only adapter that stores events in a local JSONL file
|
|
9
|
+
# and exposes a rich read API for TUI, Browser Overlay, and MCP Server.
|
|
10
|
+
#
|
|
11
|
+
# Auto-registered by Railtie in development/test environments.
|
|
12
|
+
# Do not use in production.
|
|
13
|
+
#
|
|
14
|
+
# @example Manual setup
|
|
15
|
+
# adapter = E11y::Adapters::DevLog.new(
|
|
16
|
+
# path: Rails.root.join("log", "e11y_dev.jsonl"),
|
|
17
|
+
# max_size: 50.megabytes,
|
|
18
|
+
# keep_rotated: 5
|
|
19
|
+
# )
|
|
20
|
+
class DevLog < Base
|
|
21
|
+
# @param path [String, Pathname]
|
|
22
|
+
# @param max_size [Integer] Rotation threshold in bytes (default 50 MB)
|
|
23
|
+
# @param max_lines [Integer] Rotation threshold in line count (default 10_000)
|
|
24
|
+
# @param keep_rotated [Integer] Number of .N.gz files to retain (default 5)
|
|
25
|
+
# @param enable_watcher [Boolean] Reserved for future file-watcher integration
|
|
26
|
+
def initialize(path: "log/e11y_dev.jsonl",
|
|
27
|
+
max_size: FileStore::DEFAULT_MAX_SIZE,
|
|
28
|
+
max_lines: FileStore::DEFAULT_MAX_LINES,
|
|
29
|
+
keep_rotated: FileStore::DEFAULT_KEEP_ROTATED,
|
|
30
|
+
enable_watcher: false)
|
|
31
|
+
super({})
|
|
32
|
+
@store = FileStore.new(path: path, max_size: max_size,
|
|
33
|
+
max_lines: max_lines, keep_rotated: keep_rotated)
|
|
34
|
+
@query = Query.new(@store.path)
|
|
35
|
+
@enable_watcher = enable_watcher
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Write a single event to the JSONL file.
|
|
39
|
+
#
|
|
40
|
+
# @param event_data [Hash] Event from the E11y pipeline
|
|
41
|
+
# @return [Boolean] true on success, false on error
|
|
42
|
+
def write(event_data)
|
|
43
|
+
@store.append(serialize(event_data))
|
|
44
|
+
true
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
warn "[E11y::DevLog] write failed: #{e.message}"
|
|
47
|
+
false
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# --- Read API (delegated to Query) ---
|
|
51
|
+
|
|
52
|
+
# @see Query#stored_events
|
|
53
|
+
def stored_events(limit: 1000, severity: nil, source: nil)
|
|
54
|
+
@query.stored_events(limit: limit, severity: severity, source: source)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# @see Query#find_event
|
|
58
|
+
def find_event(id) = @query.find_event(id)
|
|
59
|
+
|
|
60
|
+
# @see Query#search
|
|
61
|
+
def search(query_str, limit: 500) = @query.search(query_str, limit: limit)
|
|
62
|
+
|
|
63
|
+
# @see Query#events_by_name
|
|
64
|
+
def events_by_name(name, limit: 500)
|
|
65
|
+
@query.stored_events(limit: limit).select { |e| e["event_name"] == name }
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @see Query#events_by_severity
|
|
69
|
+
def events_by_severity(sev, limit: 500)
|
|
70
|
+
@query.stored_events(limit: limit, severity: sev)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# @see Query#events_by_trace
|
|
74
|
+
def events_by_trace(trace_id) = @query.events_by_trace(trace_id)
|
|
75
|
+
|
|
76
|
+
# @see Query#interactions
|
|
77
|
+
def interactions(window_ms: 500, limit: 50, source: nil)
|
|
78
|
+
@query.interactions(window_ms: window_ms, limit: limit, source: source)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @see Query#stats
|
|
82
|
+
def stats = @query.stats
|
|
83
|
+
|
|
84
|
+
# @see Query#updated_since?
|
|
85
|
+
def updated_since?(timestamp) = @query.updated_since?(timestamp)
|
|
86
|
+
|
|
87
|
+
# @see Query#clear!
|
|
88
|
+
def clear! = @query.clear!
|
|
89
|
+
|
|
90
|
+
# Advertise dev_log and readable capabilities.
|
|
91
|
+
def capabilities
|
|
92
|
+
super.merge(dev_log: true, readable: true)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def serialize(event_data)
|
|
98
|
+
data = event_data.is_a?(::Hash) ? event_data.transform_keys(&:to_s) : {}
|
|
99
|
+
enrich_ids!(data)
|
|
100
|
+
enrich_metadata!(data)
|
|
101
|
+
::JSON.generate(data)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def enrich_ids!(data)
|
|
105
|
+
data["id"] ||= ::SecureRandom.uuid
|
|
106
|
+
data["timestamp"] ||= ::Time.now.utc.iso8601(3)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def enrich_metadata!(data)
|
|
110
|
+
source = ::Thread.current[:e11y_source] || "web"
|
|
111
|
+
meta = (data["metadata"] || {}).dup
|
|
112
|
+
meta["source"] ||= source
|
|
113
|
+
meta["started_at"] ||= data["timestamp"]
|
|
114
|
+
data["metadata"] = meta
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
data/lib/e11y/adapters/file.rb
CHANGED
|
@@ -26,11 +26,8 @@ module E11y
|
|
|
26
26
|
#
|
|
27
27
|
# adapter.write(event_name: "user.login", severity: :info)
|
|
28
28
|
#
|
|
29
|
-
# @example
|
|
30
|
-
# E11y::Adapters::
|
|
31
|
-
# :file_logger,
|
|
32
|
-
# E11y::Adapters::File.new(path: "log/events.log")
|
|
33
|
-
# )
|
|
29
|
+
# @example Configuration
|
|
30
|
+
# config.adapters[:file] = E11y::Adapters::File.new(path: "log/events.log")
|
|
34
31
|
# rubocop:disable Metrics/ClassLength
|
|
35
32
|
# File adapter contains file rotation and buffering logic as cohesive unit
|
|
36
33
|
class File < Base
|
|
@@ -152,7 +149,7 @@ module E11y
|
|
|
152
149
|
|
|
153
150
|
# Open file for writing
|
|
154
151
|
def open_file!
|
|
155
|
-
@file = ::File.
|
|
152
|
+
@file = ::File.new(@path, "a")
|
|
156
153
|
@file.sync = true
|
|
157
154
|
@current_date = Date.today if @rotation == :daily
|
|
158
155
|
end
|
|
@@ -39,6 +39,7 @@ module E11y
|
|
|
39
39
|
# test_adapter = E11y::Adapters::InMemory.new(max_events: nil)
|
|
40
40
|
#
|
|
41
41
|
# @see ADR-004 §9.1 (In-Memory Test Adapter)
|
|
42
|
+
# rubocop:disable Metrics/ClassLength
|
|
42
43
|
class InMemory < Base
|
|
43
44
|
# Default maximum number of events to store
|
|
44
45
|
DEFAULT_MAX_EVENTS = 1000
|
|
@@ -118,17 +119,28 @@ module E11y
|
|
|
118
119
|
end
|
|
119
120
|
end
|
|
120
121
|
|
|
122
|
+
alias clear clear!
|
|
123
|
+
|
|
121
124
|
# Find events matching pattern
|
|
122
125
|
#
|
|
123
|
-
# @param pattern [String, Regexp]
|
|
126
|
+
# @param pattern [String, Regexp, Class] Event name pattern or event class
|
|
124
127
|
# @return [Array<Hash>] Matching events
|
|
125
128
|
#
|
|
126
129
|
# @example
|
|
127
130
|
# adapter.find_events(/order/) # All order.* events
|
|
128
131
|
# adapter.find_events("order.paid") # Exact match
|
|
132
|
+
# adapter.find_events(Events::OrderPaid) # By event class
|
|
129
133
|
def find_events(pattern)
|
|
130
|
-
pattern =
|
|
131
|
-
@events.select { |event|
|
|
134
|
+
pattern = event_pattern_for(pattern)
|
|
135
|
+
@events.select { |event| event_matches?(event, pattern) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Find first event matching pattern
|
|
139
|
+
#
|
|
140
|
+
# @param pattern [String, Regexp, Class] Event name pattern or event class
|
|
141
|
+
# @return [Hash, nil] First matching event or nil
|
|
142
|
+
def find_event(pattern)
|
|
143
|
+
find_events(pattern).first
|
|
132
144
|
end
|
|
133
145
|
|
|
134
146
|
# Count events by name
|
|
@@ -138,8 +150,10 @@ module E11y
|
|
|
138
150
|
#
|
|
139
151
|
# @example
|
|
140
152
|
# adapter.event_count # Total events
|
|
141
|
-
# adapter.event_count("order.paid") # Specific event count
|
|
142
|
-
|
|
153
|
+
# adapter.event_count("order.paid") # Specific event count (positional)
|
|
154
|
+
# adapter.event_count(event_name: "order.paid") # Specific event count (keyword)
|
|
155
|
+
def event_count(event_name = nil, **kwargs)
|
|
156
|
+
event_name ||= kwargs[:event_name]
|
|
143
157
|
if event_name
|
|
144
158
|
@events.count { |event| event[:event_name] == event_name }
|
|
145
159
|
else
|
|
@@ -147,6 +161,16 @@ module E11y
|
|
|
147
161
|
end
|
|
148
162
|
end
|
|
149
163
|
|
|
164
|
+
# Get the most recently written event.
|
|
165
|
+
#
|
|
166
|
+
# @return [Hash, nil] The last event, or nil if none
|
|
167
|
+
#
|
|
168
|
+
# @example
|
|
169
|
+
# adapter.last_event # Most recently written event
|
|
170
|
+
def last_event
|
|
171
|
+
events.last
|
|
172
|
+
end
|
|
173
|
+
|
|
150
174
|
# Get last N events
|
|
151
175
|
#
|
|
152
176
|
# @param count [Integer] Number of events to return
|
|
@@ -205,6 +229,28 @@ module E11y
|
|
|
205
229
|
|
|
206
230
|
private
|
|
207
231
|
|
|
232
|
+
def event_pattern_for(pattern)
|
|
233
|
+
case pattern
|
|
234
|
+
when Class then pattern
|
|
235
|
+
when String, Regexp then pattern.is_a?(String) ? Regexp.new(Regexp.escape(pattern)) : pattern
|
|
236
|
+
else raise ArgumentError, "Pattern must be Class, String, or Regexp, got #{pattern.class}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def event_matches?(event, pattern)
|
|
241
|
+
return event[:event_name].to_s.match?(pattern) if pattern.is_a?(Regexp)
|
|
242
|
+
return event_matches_class?(event, pattern) if pattern.is_a?(Class)
|
|
243
|
+
|
|
244
|
+
false
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def event_matches_class?(event, klass)
|
|
248
|
+
event[:event_class] == klass ||
|
|
249
|
+
event[:event_class]&.name == klass.name ||
|
|
250
|
+
event[:event_name].to_s == (klass.respond_to?(:event_name) ? klass.event_name : klass.name) ||
|
|
251
|
+
event[:event_name].to_s.include?(klass.name)
|
|
252
|
+
end
|
|
253
|
+
|
|
208
254
|
# Enforce max_events limit by dropping oldest events (FIFO)
|
|
209
255
|
#
|
|
210
256
|
# @return [void]
|
|
@@ -218,5 +264,6 @@ module E11y
|
|
|
218
264
|
@dropped_count += excess
|
|
219
265
|
end
|
|
220
266
|
end
|
|
267
|
+
# rubocop:enable Metrics/ClassLength
|
|
221
268
|
end
|
|
222
269
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "in_memory"
|
|
4
|
+
|
|
5
|
+
module E11y
|
|
6
|
+
module Adapters
|
|
7
|
+
# InMemoryTest Adapter — extends InMemory with test-specific helpers.
|
|
8
|
+
#
|
|
9
|
+
# Overrides `last_event` to skip Rails auto-instrumentation events
|
|
10
|
+
# (E11y::Events::Rails::*) that fire after each HTTP request and
|
|
11
|
+
# would otherwise obscure the event your test just tracked.
|
|
12
|
+
#
|
|
13
|
+
# Use this adapter in test suites; use `InMemory` in production configs.
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# let(:adapter) { E11y::Adapters::InMemoryTest.new }
|
|
17
|
+
# before { E11y.register_adapter :memory, adapter }
|
|
18
|
+
class InMemoryTest < InMemory
|
|
19
|
+
# Return the last event that was NOT fired by Rails auto-instrumentation.
|
|
20
|
+
#
|
|
21
|
+
# @return [Hash, nil]
|
|
22
|
+
def last_event
|
|
23
|
+
events.reverse_each.find do |e|
|
|
24
|
+
!e[:event_name].to_s.start_with?("E11y::Events::Rails::")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/e11y/adapters/loki.rb
CHANGED
|
@@ -41,11 +41,8 @@ module E11y
|
|
|
41
41
|
# batch_timeout: 5
|
|
42
42
|
# )
|
|
43
43
|
#
|
|
44
|
-
# @example
|
|
45
|
-
# E11y::Adapters::
|
|
46
|
-
# :loki_logger,
|
|
47
|
-
# E11y::Adapters::Loki.new(url: ENV["LOKI_URL"])
|
|
48
|
-
# )
|
|
44
|
+
# @example Configuration
|
|
45
|
+
# config.adapters[:loki] = E11y::Adapters::Loki.new(url: ENV["LOKI_URL"])
|
|
49
46
|
#
|
|
50
47
|
# @example With Cardinality Protection (C04 Resolution - Enterprise)
|
|
51
48
|
# # Enable for high-traffic environments to prevent label explosion
|
|
@@ -82,8 +79,9 @@ module E11y
|
|
|
82
79
|
# @option config [Integer] :batch_timeout (5) Max seconds to wait before flushing batch
|
|
83
80
|
# @option config [Boolean] :compress (true) Enable gzip compression
|
|
84
81
|
# @option config [String] :tenant_id (nil) Loki tenant ID (X-Scope-OrgID header)
|
|
85
|
-
# @option config [Boolean] :enable_cardinality_protection (
|
|
86
|
-
# @option config [Integer] :max_label_cardinality (
|
|
82
|
+
# @option config [Boolean] :enable_cardinality_protection (true) Enable cardinality protection for labels (C04)
|
|
83
|
+
# @option config [Integer] :max_label_cardinality (1000) Max unique values per label when protection enabled.
|
|
84
|
+
# Labels = event_name + severity only (payload stays in log line). 1000 covers ~1000 event types.
|
|
87
85
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
88
86
|
# Adapter initialization requires many instance variable assignments
|
|
89
87
|
def initialize(config = {})
|
|
@@ -91,20 +89,22 @@ module E11y
|
|
|
91
89
|
@labels = config.fetch(:labels, {})
|
|
92
90
|
@batch_size = config.fetch(:batch_size, DEFAULT_BATCH_SIZE)
|
|
93
91
|
@batch_timeout = config.fetch(:batch_timeout, DEFAULT_BATCH_TIMEOUT)
|
|
92
|
+
@timeout = config.fetch(:timeout, 5)
|
|
93
|
+
@health_check_timeout = [@timeout, 2].min
|
|
94
94
|
@compress = config.fetch(:compress, true)
|
|
95
95
|
@tenant_id = config[:tenant_id]
|
|
96
|
-
@enable_cardinality_protection = config.fetch(:enable_cardinality_protection,
|
|
97
|
-
@max_label_cardinality = config.fetch(:max_label_cardinality,
|
|
96
|
+
@enable_cardinality_protection = config.fetch(:enable_cardinality_protection, true)
|
|
97
|
+
@max_label_cardinality = config.fetch(:max_label_cardinality, 1000)
|
|
98
98
|
|
|
99
99
|
@buffer = []
|
|
100
100
|
@buffer_mutex = Mutex.new
|
|
101
101
|
@connection = nil
|
|
102
102
|
@last_flush = Time.now
|
|
103
103
|
|
|
104
|
-
# C04:
|
|
104
|
+
# C04: Cardinality protection for labels (enabled by default per ADR-009 §8)
|
|
105
105
|
if @enable_cardinality_protection
|
|
106
106
|
@cardinality_protection = E11y::Metrics::CardinalityProtection.new(
|
|
107
|
-
|
|
107
|
+
cardinality_limit: @max_label_cardinality
|
|
108
108
|
)
|
|
109
109
|
end
|
|
110
110
|
|
|
@@ -155,11 +155,22 @@ module E11y
|
|
|
155
155
|
end
|
|
156
156
|
end
|
|
157
157
|
|
|
158
|
-
#
|
|
158
|
+
# Loki health check endpoint
|
|
159
|
+
READY_PATH = "/ready"
|
|
160
|
+
|
|
161
|
+
# Check if adapter is healthy (Loki server reachable)
|
|
162
|
+
#
|
|
163
|
+
# Performs actual HTTP GET to /ready. Returns false on connection failure,
|
|
164
|
+
# timeout, or non-2xx response.
|
|
159
165
|
#
|
|
160
|
-
# @return [Boolean] True if
|
|
166
|
+
# @return [Boolean] True if Loki responds with 2xx
|
|
161
167
|
def healthy?
|
|
162
|
-
@connection
|
|
168
|
+
return false unless @connection
|
|
169
|
+
|
|
170
|
+
response = @connection.get(READY_PATH)
|
|
171
|
+
(200..299).cover?(response.status)
|
|
172
|
+
rescue Faraday::Error, Errno::ECONNREFUSED, Errno::ETIMEDOUT
|
|
173
|
+
false
|
|
163
174
|
end
|
|
164
175
|
|
|
165
176
|
# Adapter capabilities
|
|
@@ -194,7 +205,6 @@ module E11y
|
|
|
194
205
|
#
|
|
195
206
|
# @see ADR-004 Section 7.1 (Retry Policy via gem-level middleware)
|
|
196
207
|
# @see ADR-004 Section 6.1 (Connection pooling via HTTP client)
|
|
197
|
-
# rubocop:disable Metrics/MethodLength
|
|
198
208
|
# HTTP client configuration requires detailed retry and connection settings
|
|
199
209
|
def build_connection!
|
|
200
210
|
@connection = Faraday.new(url: @url) do |f|
|
|
@@ -218,7 +228,6 @@ module E11y
|
|
|
218
228
|
f.adapter Faraday.default_adapter
|
|
219
229
|
end
|
|
220
230
|
end
|
|
221
|
-
# rubocop:enable Metrics/MethodLength
|
|
222
231
|
|
|
223
232
|
# Check if buffer should be flushed
|
|
224
233
|
def flush_if_needed!
|
|
@@ -280,22 +289,23 @@ module E11y
|
|
|
280
289
|
|
|
281
290
|
# Extract labels from event
|
|
282
291
|
#
|
|
292
|
+
# Uses normalized event_name (e.g., "Events::TestLoki" -> "test.loki") for consistent
|
|
293
|
+
# querying via LogQL. Matches Versioning middleware convention.
|
|
294
|
+
#
|
|
283
295
|
# @param event_data [Hash] Event data
|
|
284
296
|
# @return [Hash] Labels for Loki stream
|
|
285
297
|
def extract_labels(event_data)
|
|
286
298
|
event_labels = {
|
|
287
|
-
event_name: event_data[:event_name].to_s,
|
|
299
|
+
event_name: normalize_event_name_for_labels(event_data[:event_name].to_s),
|
|
288
300
|
severity: event_data[:severity].to_s
|
|
289
301
|
}
|
|
290
302
|
|
|
291
303
|
# Merge static and event labels
|
|
292
304
|
all_labels = @labels.merge(event_labels)
|
|
293
305
|
|
|
294
|
-
# C04:
|
|
295
|
-
#
|
|
296
|
-
if @enable_cardinality_protection && @cardinality_protection
|
|
297
|
-
all_labels = @cardinality_protection.filter(all_labels, "loki.stream")
|
|
298
|
-
end
|
|
306
|
+
# C04: Cardinality protection for labels only. Labels = event_name + severity (payload
|
|
307
|
+
# stays in log line). Filter by user_uuid via LogQL: | json | user_uuid="xxx"
|
|
308
|
+
all_labels = @cardinality_protection.filter(all_labels, "loki.stream") if @enable_cardinality_protection && @cardinality_protection
|
|
299
309
|
|
|
300
310
|
all_labels.transform_keys(&:to_s)
|
|
301
311
|
end
|
|
@@ -305,7 +315,17 @@ module E11y
|
|
|
305
315
|
# @param event_data [Hash] Event data
|
|
306
316
|
# @return [Array] [timestamp_ns, line]
|
|
307
317
|
def format_loki_entry(event_data)
|
|
308
|
-
|
|
318
|
+
# Parse timestamp - can be Time object, ISO8601 string, or nil
|
|
319
|
+
timestamp = event_data[:timestamp]
|
|
320
|
+
timestamp = if timestamp.is_a?(String)
|
|
321
|
+
Time.parse(timestamp)
|
|
322
|
+
elsif timestamp.nil?
|
|
323
|
+
Time.now
|
|
324
|
+
else
|
|
325
|
+
timestamp
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
timestamp_ns = timestamp.to_f * 1_000_000_000
|
|
309
329
|
line = event_data.to_json
|
|
310
330
|
|
|
311
331
|
[timestamp_ns.to_i.to_s, line]
|
|
@@ -323,6 +343,21 @@ module E11y
|
|
|
323
343
|
io.string
|
|
324
344
|
end
|
|
325
345
|
|
|
346
|
+
# Normalize event name for Loki labels (matches Versioning middleware convention)
|
|
347
|
+
#
|
|
348
|
+
# @param name [String] Event name (e.g., "Events::TestLoki")
|
|
349
|
+
# @return [String] Normalized name (e.g., "test.loki")
|
|
350
|
+
def normalize_event_name_for_labels(name)
|
|
351
|
+
return name if name.nil? || name.empty?
|
|
352
|
+
|
|
353
|
+
n = name.sub(/^Events::/, "").sub(/V\d+$/, "")
|
|
354
|
+
n.gsub("::", ".")
|
|
355
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
356
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
357
|
+
.downcase
|
|
358
|
+
.tr("_", ".")
|
|
359
|
+
end
|
|
360
|
+
|
|
326
361
|
# Build HTTP headers
|
|
327
362
|
#
|
|
328
363
|
# @return [Hash] Headers for Loki request
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Adapters
|
|
5
|
+
# Null Adapter — silently discards all events.
|
|
6
|
+
#
|
|
7
|
+
# Designed for use in tests and development environments where you want
|
|
8
|
+
# to suppress all output while still being able to assert that events
|
|
9
|
+
# were tracked (via the `events` reader).
|
|
10
|
+
#
|
|
11
|
+
# @example In tests
|
|
12
|
+
# RSpec.configure do |config|
|
|
13
|
+
# config.before do
|
|
14
|
+
# E11y.configure do |c|
|
|
15
|
+
# c.adapters[:null] = E11y::Adapters::NullAdapter.new
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# @example Asserting events
|
|
21
|
+
# null_adapter = E11y::Adapters::NullAdapter.new
|
|
22
|
+
# E11y.configure { |c| c.adapters[:null] = null_adapter }
|
|
23
|
+
#
|
|
24
|
+
# Events::OrderPaid.track(order_id: "123", amount: 99.99)
|
|
25
|
+
#
|
|
26
|
+
# expect(null_adapter.events.size).to eq(1)
|
|
27
|
+
# expect(null_adapter.events.last[:event_name]).to eq("order.paid")
|
|
28
|
+
class Null < Base
|
|
29
|
+
attr_reader :events
|
|
30
|
+
|
|
31
|
+
# @param config [Hash] Options
|
|
32
|
+
# @option config [Boolean] :store_events (true) When false, truly discards (no retention).
|
|
33
|
+
# Use store_events: false for memory profiling to measure pipeline-only allocations.
|
|
34
|
+
def initialize(config = {})
|
|
35
|
+
super
|
|
36
|
+
@store_events = config.fetch(:store_events, true)
|
|
37
|
+
@events = []
|
|
38
|
+
@mutex = Mutex.new
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Accept event. When store_events: true, stores for inspection. When false, truly discards.
|
|
42
|
+
#
|
|
43
|
+
# @param event_data [Hash] Event payload
|
|
44
|
+
# @return [Boolean] always true
|
|
45
|
+
# rubocop:disable Naming/PredicateMethod -- implements Base adapter interface
|
|
46
|
+
def write(event_data)
|
|
47
|
+
@mutex.synchronize { @events << event_data.dup } if @store_events
|
|
48
|
+
true
|
|
49
|
+
end
|
|
50
|
+
# rubocop:enable Naming/PredicateMethod
|
|
51
|
+
|
|
52
|
+
# Accept batch. When store_events: true, stores for inspection. When false, truly discards.
|
|
53
|
+
#
|
|
54
|
+
# @param events [Array<Hash>] Event payloads
|
|
55
|
+
# @return [Boolean] always true
|
|
56
|
+
# rubocop:disable Naming/PredicateMethod -- implements Base adapter interface
|
|
57
|
+
def write_batch(events)
|
|
58
|
+
@mutex.synchronize { @events.concat(events.map(&:dup)) } if @store_events
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
# rubocop:enable Naming/PredicateMethod
|
|
62
|
+
|
|
63
|
+
# Clear all stored events (useful between test examples).
|
|
64
|
+
#
|
|
65
|
+
# @return [void]
|
|
66
|
+
def clear!
|
|
67
|
+
@mutex.synchronize { @events.clear }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def healthy?
|
|
71
|
+
true
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def capabilities
|
|
75
|
+
{ batching: true, compression: false, async: false, streaming: false, null: true }
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Convenience alias matching Quick Start documentation.
|
|
80
|
+
NullAdapter = Null
|
|
81
|
+
end
|
|
82
|
+
end
|