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,183 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# OTLP HTTP adapter — requires Faraday
|
|
4
|
+
begin
|
|
5
|
+
require "faraday"
|
|
6
|
+
rescue LoadError
|
|
7
|
+
raise LoadError, <<~ERROR
|
|
8
|
+
Faraday not available!
|
|
9
|
+
|
|
10
|
+
To use E11y::Adapters::OpenTelemetryCollector, add to your Gemfile:
|
|
11
|
+
|
|
12
|
+
gem 'faraday'
|
|
13
|
+
|
|
14
|
+
Then run: bundle install
|
|
15
|
+
ERROR
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
require "e11y/opentelemetry/semantic_conventions"
|
|
19
|
+
|
|
20
|
+
module E11y
|
|
21
|
+
module Adapters
|
|
22
|
+
# OpenTelemetry Collector adapter (ADR-007 §3, F1)
|
|
23
|
+
#
|
|
24
|
+
# Sends E11y events to OpenTelemetry Collector via OTLP HTTP.
|
|
25
|
+
# No OpenTelemetry SDK required — uses raw HTTP (Faraday).
|
|
26
|
+
#
|
|
27
|
+
# **Use case:** When you want to send logs to OTel Collector without
|
|
28
|
+
# loading the full OTel SDK (e.g. lightweight apps, or OTelLogs already
|
|
29
|
+
# handles in-process; this adapter sends to external Collector).
|
|
30
|
+
#
|
|
31
|
+
# @example Configuration
|
|
32
|
+
# E11y.configure do |config|
|
|
33
|
+
# config.adapters[:otel_collector] = E11y::Adapters::OpenTelemetryCollector.new(
|
|
34
|
+
# endpoint: "http://localhost:4318",
|
|
35
|
+
# service_name: "my-app"
|
|
36
|
+
# )
|
|
37
|
+
# end
|
|
38
|
+
#
|
|
39
|
+
# @see ADR-007 §3 OTel Collector Adapter
|
|
40
|
+
class OpenTelemetryCollector < Base
|
|
41
|
+
SEVERITY_MAPPING = {
|
|
42
|
+
debug: 5, info: 9, success: 9, warn: 13, error: 17, fatal: 21
|
|
43
|
+
}.freeze
|
|
44
|
+
|
|
45
|
+
def initialize(endpoint: nil, service_name: nil, headers: {}, timeout: 10, max_attributes: 50, compress: true, **)
|
|
46
|
+
super(**)
|
|
47
|
+
@endpoint = (endpoint || ENV["OTEL_EXPORTER_OTLP_ENDPOINT"] || "http://localhost:4318").chomp("/")
|
|
48
|
+
@service_name = service_name || E11y.config&.service_name || "e11y"
|
|
49
|
+
@headers = headers
|
|
50
|
+
@timeout = timeout
|
|
51
|
+
@max_attributes = max_attributes
|
|
52
|
+
@compress = compress
|
|
53
|
+
@connection = build_connection
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def write(event_data)
|
|
57
|
+
payload = build_otlp_payload([event_data])
|
|
58
|
+
body = payload.to_json
|
|
59
|
+
body = compress_body(body) if @compress
|
|
60
|
+
|
|
61
|
+
response = @connection.post("/v1/logs") do |req|
|
|
62
|
+
req.headers["Content-Type"] = "application/json"
|
|
63
|
+
req.headers["Content-Encoding"] = "gzip" if @compress
|
|
64
|
+
req.body = body
|
|
65
|
+
end
|
|
66
|
+
response.success?
|
|
67
|
+
rescue Faraday::Error => e
|
|
68
|
+
warn "[E11y::OpenTelemetryCollector] HTTP error: #{e.message}"
|
|
69
|
+
false
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def healthy?
|
|
73
|
+
!@connection.nil?
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def capabilities
|
|
77
|
+
{ batching: false, compression: @compress, async: false, streaming: false }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def compress_body(body)
|
|
83
|
+
io = StringIO.new
|
|
84
|
+
gz = Zlib::GzipWriter.new(io)
|
|
85
|
+
gz.write(body)
|
|
86
|
+
gz.close
|
|
87
|
+
io.string
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def build_connection
|
|
91
|
+
Faraday.new(url: @endpoint, request: { timeout: @timeout }) do |f|
|
|
92
|
+
@headers.each { |k, v| f.headers[k.to_s] = v }
|
|
93
|
+
f.adapter Faraday.default_adapter
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def build_otlp_payload(events)
|
|
98
|
+
log_records = events.map { |e| to_otel_log_record(e) }
|
|
99
|
+
{
|
|
100
|
+
resourceLogs: [{
|
|
101
|
+
resource: { attributes: resource_attributes },
|
|
102
|
+
scopeLogs: [{
|
|
103
|
+
scope: { name: "e11y", version: E11y::VERSION },
|
|
104
|
+
logRecords: log_records
|
|
105
|
+
}]
|
|
106
|
+
}]
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def resource_attributes
|
|
111
|
+
[
|
|
112
|
+
{ key: "service.name", value: { stringValue: @service_name } },
|
|
113
|
+
{ key: "service.version", value: { stringValue: E11y::VERSION } },
|
|
114
|
+
{ key: "deployment.environment", value: { stringValue: E11y.config&.environment || ENV["RAILS_ENV"] || "development" } },
|
|
115
|
+
{ key: "host.name", value: { stringValue: hostname } },
|
|
116
|
+
{ key: "process.pid", value: { intValue: Process.pid.to_s } }
|
|
117
|
+
]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def hostname
|
|
121
|
+
require "socket"
|
|
122
|
+
Socket.gethostname
|
|
123
|
+
rescue StandardError
|
|
124
|
+
ENV["HOSTNAME"] || "unknown"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def to_otel_log_record(event)
|
|
128
|
+
ts = event[:timestamp] || Time.now.utc
|
|
129
|
+
ts_nano = (ts.to_f * 1_000_000_000).to_i
|
|
130
|
+
{
|
|
131
|
+
timeUnixNano: ts_nano.to_s,
|
|
132
|
+
observedTimeUnixNano: (Time.now.to_f * 1_000_000_000).to_i.to_s,
|
|
133
|
+
severityNumber: SEVERITY_MAPPING[event[:severity]] || 9,
|
|
134
|
+
severityText: (event[:severity] || :info).to_s.upcase,
|
|
135
|
+
body: { stringValue: event[:event_name] },
|
|
136
|
+
attributes: build_log_attributes(event),
|
|
137
|
+
traceId: encode_hex(event[:trace_id], 32),
|
|
138
|
+
spanId: encode_hex(event[:span_id], 16)
|
|
139
|
+
}.compact
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def build_log_attributes(event)
|
|
143
|
+
attrs = []
|
|
144
|
+
attrs << { key: "event.name", value: { stringValue: event[:event_name] } }
|
|
145
|
+
attrs << { key: "event.version", value: { stringValue: event[:v].to_s } } if event[:v]
|
|
146
|
+
attrs << { key: "service.name", value: { stringValue: @service_name } }
|
|
147
|
+
|
|
148
|
+
payload = event[:payload] || {}
|
|
149
|
+
payload.each do |key, value|
|
|
150
|
+
break if attrs.size >= @max_attributes
|
|
151
|
+
|
|
152
|
+
otel_key = E11y::OpenTelemetry::SemanticConventions.map_key(event[:event_name], key)
|
|
153
|
+
attrs << encode_attr(otel_key, value)
|
|
154
|
+
end
|
|
155
|
+
attrs
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def encode_attr(key, value)
|
|
159
|
+
case value
|
|
160
|
+
when String
|
|
161
|
+
{ key: key.to_s, value: { stringValue: value } }
|
|
162
|
+
when Integer
|
|
163
|
+
{ key: key.to_s, value: { intValue: value.to_s } }
|
|
164
|
+
when Float
|
|
165
|
+
{ key: key.to_s, value: { doubleValue: value } }
|
|
166
|
+
when TrueClass, FalseClass
|
|
167
|
+
{ key: key.to_s, value: { boolValue: value } }
|
|
168
|
+
else
|
|
169
|
+
{ key: key.to_s, value: { stringValue: value.to_s } }
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def encode_hex(str, expected_len)
|
|
174
|
+
return nil if str.to_s.empty?
|
|
175
|
+
|
|
176
|
+
s = str.to_s.gsub(/[^0-9a-fA-F]/, "")
|
|
177
|
+
return nil if s.length != expected_len
|
|
178
|
+
|
|
179
|
+
s.downcase
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "e11y/opentelemetry/semantic_conventions"
|
|
4
|
+
|
|
3
5
|
# Check if OpenTelemetry SDK is available
|
|
4
6
|
begin
|
|
5
7
|
require "opentelemetry/sdk"
|
|
6
8
|
require "opentelemetry/logs"
|
|
9
|
+
require "opentelemetry-logs-sdk" # Provides OpenTelemetry::SDK::Logs::LoggerProvider
|
|
7
10
|
rescue LoadError
|
|
8
11
|
raise LoadError, <<~ERROR
|
|
9
12
|
OpenTelemetry SDK not available!
|
|
@@ -11,7 +14,8 @@ rescue LoadError
|
|
|
11
14
|
To use E11y::Adapters::OTelLogs, add to your Gemfile:
|
|
12
15
|
|
|
13
16
|
gem 'opentelemetry-sdk'
|
|
14
|
-
gem 'opentelemetry-logs'
|
|
17
|
+
gem 'opentelemetry-logs-api'
|
|
18
|
+
gem 'opentelemetry-logs-sdk'
|
|
15
19
|
|
|
16
20
|
Then run: bundle install
|
|
17
21
|
ERROR
|
|
@@ -58,6 +62,12 @@ module E11y
|
|
|
58
62
|
# @see ADR-007 for OpenTelemetry integration architecture
|
|
59
63
|
# @see UC-008 for use cases
|
|
60
64
|
class OTelLogs < Base
|
|
65
|
+
# Struct for test assertions (replaces OpenStruct per Style/OpenStructUse)
|
|
66
|
+
LogRecordStruct = Struct.new(
|
|
67
|
+
:timestamp, :observed_timestamp, :severity_number, :severity_text,
|
|
68
|
+
:body, :attributes, :trace_id, :span_id, :trace_flags
|
|
69
|
+
)
|
|
70
|
+
|
|
61
71
|
# E11y severity → OTel severity_number mapping
|
|
62
72
|
# See: https://opentelemetry.io/docs/specs/otel/logs/data-model/#field-severitynumber
|
|
63
73
|
# Severity numbers: TRACE=1, DEBUG=5, INFO=9, WARN=13, ERROR=17, FATAL=21
|
|
@@ -70,7 +80,8 @@ module E11y
|
|
|
70
80
|
fatal: 21 # FATAL
|
|
71
81
|
}.freeze
|
|
72
82
|
|
|
73
|
-
# Default baggage allowlist
|
|
83
|
+
# Default baggage allowlist kept for reference / backward compat.
|
|
84
|
+
# @deprecated Pass baggage_allowlist: :all (the new default) or an explicit Array.
|
|
74
85
|
DEFAULT_BAGGAGE_ALLOWLIST = %i[
|
|
75
86
|
trace_id
|
|
76
87
|
span_id
|
|
@@ -82,24 +93,45 @@ module E11y
|
|
|
82
93
|
# Initialize OTel Logs adapter
|
|
83
94
|
#
|
|
84
95
|
# @param service_name [String] Service name for OTel (default: from config)
|
|
85
|
-
# @param baggage_allowlist [Array<Symbol
|
|
86
|
-
#
|
|
87
|
-
|
|
96
|
+
# @param baggage_allowlist [Array<Symbol>, :all] Keys to include in OTel attributes.
|
|
97
|
+
# `:all` (default) passes every payload key — PII is already stripped upstream by
|
|
98
|
+
# Middleware::PIIFilter before the adapter is called.
|
|
99
|
+
# Pass an explicit Array for stricter filtering (backward compat).
|
|
100
|
+
# @param max_attributes [Integer] Max attributes per log (cardinality limit)
|
|
101
|
+
# @param cardinality_protection [Boolean] Use full 3-layer protection (C04). Default false for
|
|
102
|
+
# logs (preserves user_id, order_id for debugging). Set true for cost-sensitive OTLP backends.
|
|
103
|
+
# @param endpoint [String, nil] OTLP endpoint (e.g. http://localhost:4318/v1/logs).
|
|
104
|
+
# When set, logs are exported to OTel Collector. Default: in-process only.
|
|
105
|
+
def initialize(service_name: nil, baggage_allowlist: :all, max_attributes: 50, cardinality_protection: false, endpoint: nil, **)
|
|
88
106
|
super(**)
|
|
89
107
|
@service_name = service_name
|
|
90
108
|
@baggage_allowlist = baggage_allowlist
|
|
91
109
|
@max_attributes = max_attributes
|
|
110
|
+
@endpoint = endpoint
|
|
111
|
+
@use_cardinality_protection = cardinality_protection
|
|
112
|
+
|
|
113
|
+
if @use_cardinality_protection
|
|
114
|
+
require "e11y/metrics/cardinality_protection"
|
|
115
|
+
@cardinality_protection = E11y::Metrics::CardinalityProtection.new(
|
|
116
|
+
cardinality_limit: 1000,
|
|
117
|
+
overflow_strategy: :drop
|
|
118
|
+
)
|
|
119
|
+
else
|
|
120
|
+
@cardinality_protection = nil
|
|
121
|
+
end
|
|
92
122
|
|
|
93
123
|
setup_logger_provider
|
|
94
124
|
end
|
|
95
125
|
|
|
96
126
|
# Write event to OTel Logs API
|
|
97
127
|
#
|
|
128
|
+
# Uses Logger#on_emit (OTel SDK 0.4+) with keyword arguments.
|
|
129
|
+
#
|
|
98
130
|
# @param event_data [Hash] Event payload
|
|
99
131
|
# @return [Boolean] true on success
|
|
100
132
|
def write(event_data)
|
|
101
|
-
|
|
102
|
-
@logger.
|
|
133
|
+
params = build_log_record_params(event_data)
|
|
134
|
+
@logger.on_emit(**params)
|
|
103
135
|
true
|
|
104
136
|
rescue StandardError => e
|
|
105
137
|
warn "[E11y::OTelLogs] Failed to write event: #{e.message}"
|
|
@@ -129,19 +161,73 @@ module E11y
|
|
|
129
161
|
|
|
130
162
|
# Setup OTel Logger Provider
|
|
131
163
|
def setup_logger_provider
|
|
132
|
-
|
|
164
|
+
resource = build_resource
|
|
165
|
+
@logger_provider = ::OpenTelemetry::SDK::Logs::LoggerProvider.new(resource: resource)
|
|
166
|
+
|
|
167
|
+
# Add OTLP exporter when endpoint configured (sends to OTel Collector)
|
|
168
|
+
if @endpoint
|
|
169
|
+
require "opentelemetry-exporter-otlp-logs"
|
|
170
|
+
exporter = ::OpenTelemetry::Exporter::OTLP::Logs::LogsExporter.new(endpoint: @endpoint)
|
|
171
|
+
processor = ::OpenTelemetry::SDK::Logs::Export::BatchLogRecordProcessor.new(exporter)
|
|
172
|
+
@logger_provider.add_log_record_processor(processor)
|
|
173
|
+
end
|
|
174
|
+
|
|
133
175
|
@logger = @logger_provider.logger(
|
|
134
176
|
name: "e11y",
|
|
135
177
|
version: E11y::VERSION
|
|
136
178
|
)
|
|
179
|
+
rescue LoadError => e
|
|
180
|
+
warn "[E11y::OTelLogs] OTLP export requested but opentelemetry-exporter-otlp-logs not available: #{e.message}"
|
|
181
|
+
resource = build_resource
|
|
182
|
+
@logger_provider ||= ::OpenTelemetry::SDK::Logs::LoggerProvider.new(resource: resource)
|
|
183
|
+
@logger = @logger_provider.logger(name: "e11y", version: E11y::VERSION)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Build OTel Resource with full attributes (ADR-007 §7, F5).
|
|
187
|
+
#
|
|
188
|
+
# @return [::OpenTelemetry::SDK::Resources::Resource]
|
|
189
|
+
def build_resource
|
|
190
|
+
attrs = {}
|
|
191
|
+
|
|
192
|
+
# Service (required)
|
|
193
|
+
attrs["service.name"] = @service_name || E11y.config&.service_name || "e11y"
|
|
194
|
+
attrs["service.version"] = E11y::VERSION
|
|
195
|
+
|
|
196
|
+
# Deployment
|
|
197
|
+
attrs["deployment.environment"] = E11y.config&.environment || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
|
|
198
|
+
|
|
199
|
+
# Host
|
|
200
|
+
attrs["host.name"] = hostname
|
|
201
|
+
|
|
202
|
+
# Process
|
|
203
|
+
attrs["process.pid"] = Process.pid
|
|
204
|
+
|
|
205
|
+
# Merge with OTel default (process.runtime, telemetry.sdk) when available
|
|
206
|
+
base = ::OpenTelemetry::SDK::Resources::Resource
|
|
207
|
+
resource = base.create(attrs)
|
|
208
|
+
resource = base.default.merge(resource) if base.respond_to?(:default)
|
|
209
|
+
resource
|
|
210
|
+
rescue StandardError
|
|
211
|
+
# Fallback: minimal resource
|
|
212
|
+
::OpenTelemetry::SDK::Resources::Resource.create(
|
|
213
|
+
"service.name" => @service_name || "e11y",
|
|
214
|
+
"service.version" => E11y::VERSION
|
|
215
|
+
)
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def hostname
|
|
219
|
+
require "socket"
|
|
220
|
+
Socket.gethostname
|
|
221
|
+
rescue StandardError
|
|
222
|
+
ENV["HOSTNAME"] || "unknown"
|
|
137
223
|
end
|
|
138
224
|
|
|
139
|
-
# Build
|
|
225
|
+
# Build params for Logger#on_emit from E11y event
|
|
140
226
|
#
|
|
141
227
|
# @param event_data [Hash] E11y event payload
|
|
142
|
-
# @return [
|
|
143
|
-
def
|
|
144
|
-
|
|
228
|
+
# @return [Hash] Keyword args for on_emit
|
|
229
|
+
def build_log_record_params(event_data)
|
|
230
|
+
{
|
|
145
231
|
timestamp: event_data[:timestamp] || Time.now.utc,
|
|
146
232
|
observed_timestamp: Time.now.utc,
|
|
147
233
|
severity_number: map_severity(event_data[:severity]),
|
|
@@ -151,7 +237,16 @@ module E11y
|
|
|
151
237
|
trace_id: event_data[:trace_id],
|
|
152
238
|
span_id: event_data[:span_id],
|
|
153
239
|
trace_flags: nil
|
|
154
|
-
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Build log record struct for testing (same data as build_log_record_params)
|
|
244
|
+
#
|
|
245
|
+
# @param event_data [Hash] E11y event payload
|
|
246
|
+
# @return [LogRecordStruct] Struct with attributes for test assertions
|
|
247
|
+
def build_log_record(event_data)
|
|
248
|
+
params = build_log_record_params(event_data)
|
|
249
|
+
LogRecordStruct.new(**params)
|
|
155
250
|
end
|
|
156
251
|
|
|
157
252
|
# Map E11y severity to OTel severity
|
|
@@ -165,39 +260,57 @@ module E11y
|
|
|
165
260
|
# Build OTel attributes from E11y payload
|
|
166
261
|
#
|
|
167
262
|
# Applies:
|
|
263
|
+
# - Semantic conventions (ADR-007 §4, F4) — maps known keys to OTel semantic names
|
|
168
264
|
# - Cardinality protection (C04 Resolution)
|
|
169
|
-
# -
|
|
265
|
+
# - Optional baggage allowlist filter (C08 Resolution — pass an Array to enable)
|
|
266
|
+
#
|
|
267
|
+
# By default (`baggage_allowlist: :all`) all payload keys are included.
|
|
268
|
+
# PII fields are stripped upstream by Middleware::PIIFilter before any adapter
|
|
269
|
+
# is called, so no additional filtering is needed at this layer.
|
|
170
270
|
#
|
|
171
271
|
# @param event_data [Hash] E11y event payload
|
|
172
272
|
# @return [Hash] OTel attributes
|
|
173
273
|
def build_attributes(event_data)
|
|
174
274
|
attributes = {}
|
|
175
275
|
|
|
176
|
-
# Add event metadata
|
|
276
|
+
# Add event metadata (low cardinality)
|
|
177
277
|
attributes["event.name"] = event_data[:event_name]
|
|
178
278
|
attributes["event.version"] = event_data[:v] if event_data[:v]
|
|
179
279
|
attributes["service.name"] = @service_name if @service_name
|
|
180
280
|
|
|
181
|
-
# Add payload (with cardinality protection)
|
|
182
281
|
payload = event_data[:payload] || {}
|
|
183
|
-
payload.each do |key, value|
|
|
184
|
-
# C04: Cardinality protection - limit attributes
|
|
185
|
-
break if attributes.size >= @max_attributes
|
|
186
282
|
|
|
187
|
-
|
|
283
|
+
# C04: Optional cardinality protection (denylist + per-key limits). Off by default for logs.
|
|
284
|
+
if @cardinality_protection
|
|
285
|
+
payload_symbols = payload.transform_keys { |k| k.to_s.to_sym }
|
|
286
|
+
payload = @cardinality_protection.filter(payload_symbols, "otel_logs")
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# Map payload to OTel semantic keys (F4)
|
|
290
|
+
payload.each do |key, value|
|
|
188
291
|
next unless baggage_allowed?(key)
|
|
189
292
|
|
|
190
|
-
|
|
293
|
+
otel_key = E11y::OpenTelemetry::SemanticConventions.map_key(
|
|
294
|
+
event_data[:event_name],
|
|
295
|
+
key
|
|
296
|
+
)
|
|
297
|
+
attributes[otel_key] = value
|
|
298
|
+
break if attributes.size >= @max_attributes
|
|
191
299
|
end
|
|
192
300
|
|
|
193
301
|
attributes
|
|
194
302
|
end
|
|
195
303
|
|
|
196
|
-
# Check if key is allowed in baggage
|
|
304
|
+
# Check if key is allowed in baggage.
|
|
305
|
+
#
|
|
306
|
+
# Returns true when allowlist is :all (default).
|
|
307
|
+
# Returns true only for listed keys when an explicit Array was configured.
|
|
197
308
|
#
|
|
198
309
|
# @param key [Symbol, String] Attribute key
|
|
199
|
-
# @return [Boolean]
|
|
310
|
+
# @return [Boolean]
|
|
200
311
|
def baggage_allowed?(key)
|
|
312
|
+
return true if @baggage_allowlist == :all
|
|
313
|
+
|
|
201
314
|
@baggage_allowlist.include?(key.to_sym)
|
|
202
315
|
end
|
|
203
316
|
end
|
data/lib/e11y/adapters/sentry.rb
CHANGED
|
@@ -33,11 +33,8 @@ module E11y
|
|
|
33
33
|
# severity_threshold: :warn
|
|
34
34
|
# )
|
|
35
35
|
#
|
|
36
|
-
# @example
|
|
37
|
-
# E11y::Adapters::
|
|
38
|
-
# :error_tracker,
|
|
39
|
-
# E11y::Adapters::Sentry.new(dsn: ENV["SENTRY_DSN"])
|
|
40
|
-
# )
|
|
36
|
+
# @example Configuration
|
|
37
|
+
# config.adapters[:sentry] = E11y::Adapters::Sentry.new(dsn: ENV["SENTRY_DSN"])
|
|
41
38
|
#
|
|
42
39
|
# @see https://docs.sentry.io/platforms/ruby/
|
|
43
40
|
# rubocop:disable Metrics/ClassLength
|
|
@@ -155,7 +152,7 @@ module E11y
|
|
|
155
152
|
# Send error to Sentry
|
|
156
153
|
#
|
|
157
154
|
# @param event_data [Hash] Event data
|
|
158
|
-
# rubocop:disable Metrics/AbcSize
|
|
155
|
+
# rubocop:disable Metrics/AbcSize
|
|
159
156
|
# Sentry scope configuration requires multiple context enrichment steps
|
|
160
157
|
def send_error_to_sentry(event_data)
|
|
161
158
|
::Sentry.with_scope do |scope|
|
|
@@ -187,7 +184,7 @@ module E11y
|
|
|
187
184
|
end
|
|
188
185
|
end
|
|
189
186
|
end
|
|
190
|
-
# rubocop:enable Metrics/AbcSize
|
|
187
|
+
# rubocop:enable Metrics/AbcSize
|
|
191
188
|
|
|
192
189
|
# Send breadcrumb to Sentry
|
|
193
190
|
#
|
data/lib/e11y/adapters/stdout.rb
CHANGED
|
@@ -41,11 +41,12 @@ module E11y
|
|
|
41
41
|
#
|
|
42
42
|
# @param config [Hash] Configuration options
|
|
43
43
|
# @option config [Boolean] :colorize (true) Enable colored output
|
|
44
|
-
# @option config [Boolean] :pretty_print (true) Enable pretty-printed JSON
|
|
44
|
+
# @option config [Boolean] :pretty_print (true) Enable pretty-printed JSON (when format: :json)
|
|
45
|
+
# @option config [Symbol] :format (:json) Output format: :json (JSON), :compact (single-line JSON), :rich (ADR-010 §3 structured)
|
|
45
46
|
def initialize(config = {})
|
|
46
47
|
@colorize = config.fetch(:colorize, true)
|
|
47
|
-
@
|
|
48
|
-
|
|
48
|
+
@format = config.fetch(:format, :json)
|
|
49
|
+
@pretty_print = resolve_pretty_print(config)
|
|
49
50
|
super
|
|
50
51
|
end
|
|
51
52
|
|
|
@@ -82,15 +83,29 @@ module E11y
|
|
|
82
83
|
|
|
83
84
|
private
|
|
84
85
|
|
|
86
|
+
# Resolve pretty_print from format or pretty_print keys
|
|
87
|
+
#
|
|
88
|
+
# @param config [Hash] Adapter config
|
|
89
|
+
# @return [Boolean]
|
|
90
|
+
def resolve_pretty_print(config)
|
|
91
|
+
return config[:pretty_print] if config.key?(:pretty_print)
|
|
92
|
+
|
|
93
|
+
case config[:format]
|
|
94
|
+
when :compact then false
|
|
95
|
+
when :pretty then true
|
|
96
|
+
else config.fetch(:pretty_print, true)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
85
100
|
# Format event for console output
|
|
86
101
|
#
|
|
87
102
|
# @param event_data [Hash] Event data
|
|
88
103
|
# @return [String] Formatted output
|
|
89
104
|
def format_event(event_data)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
case @format
|
|
106
|
+
when :rich then format_event_rich(event_data)
|
|
107
|
+
when :compact then event_data.to_json
|
|
108
|
+
else @pretty_print ? JSON.pretty_generate(event_data) : event_data.to_json
|
|
94
109
|
end
|
|
95
110
|
end
|
|
96
111
|
|
|
@@ -103,6 +118,57 @@ module E11y
|
|
|
103
118
|
color_code = SEVERITY_COLORS[severity] || ""
|
|
104
119
|
"#{color_code}#{output}#{COLOR_RESET}"
|
|
105
120
|
end
|
|
121
|
+
|
|
122
|
+
# Rich format: ADR-010 §3 — structured output with header, event name, payload, metadata
|
|
123
|
+
def format_event_rich(event_data)
|
|
124
|
+
lines = []
|
|
125
|
+
lines << format_header(event_data)
|
|
126
|
+
lines << format_event_name_line(event_data)
|
|
127
|
+
lines << format_payload_section(event_data[:payload]) if event_data[:payload]&.any?
|
|
128
|
+
lines << format_metadata_section(event_data) if event_data[:trace_id] || event_data[:span_id]
|
|
129
|
+
lines << ("─" * 80)
|
|
130
|
+
lines.join("\n")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def format_header(event_data)
|
|
134
|
+
ts = event_data[:timestamp]
|
|
135
|
+
ts = Time.parse(ts) if ts.is_a?(String)
|
|
136
|
+
time_str = ts&.strftime("%H:%M:%S.%L") || "??:??:??.???"
|
|
137
|
+
sev = event_data[:severity].to_s.upcase.ljust(8)
|
|
138
|
+
"#{time_str} #{sev}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def format_event_name_line(event_data)
|
|
142
|
+
name = event_data[:event_name].to_s
|
|
143
|
+
" → #{name}"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def format_payload_section(payload)
|
|
147
|
+
lines = [" Payload:"]
|
|
148
|
+
payload.each do |k, v|
|
|
149
|
+
lines << " #{k}: #{format_value_rich(v)}"
|
|
150
|
+
end
|
|
151
|
+
lines.join("\n")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def format_metadata_section(event_data)
|
|
155
|
+
meta = { trace_id: event_data[:trace_id], span_id: event_data[:span_id] }.compact
|
|
156
|
+
return "" if meta.empty?
|
|
157
|
+
|
|
158
|
+
meta.map { |k, v| " #{k}: #{v}" }.unshift(" Metadata:").join("\n")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def format_value_rich(value)
|
|
162
|
+
case value
|
|
163
|
+
when String then "\"#{value.length > 50 ? "#{value[0...50]}..." : value}\""
|
|
164
|
+
when Array then "[#{value.size} items]"
|
|
165
|
+
when Hash then "{#{value.size} keys}"
|
|
166
|
+
else value.inspect
|
|
167
|
+
end
|
|
168
|
+
end
|
|
106
169
|
end
|
|
170
|
+
|
|
171
|
+
# Alias for ADR-010 §3 (Console Output) — Console and Stdout are the same adapter
|
|
172
|
+
Console = Stdout
|
|
107
173
|
end
|
|
108
174
|
end
|