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.
Files changed (288) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +130 -10
  3. data/CHANGELOG.md +80 -1
  4. data/CLAUDE.md +168 -0
  5. data/CONTRIBUTING.md +640 -0
  6. data/README.md +165 -701
  7. data/RELEASE.md +41 -12
  8. data/Rakefile +249 -57
  9. data/config/README.md +1 -1
  10. data/config/loki-local-config.yaml +12 -0
  11. data/config/otel-collector-config.yaml +44 -0
  12. data/cucumber.yml +1 -0
  13. data/docker-compose.yml +18 -2
  14. data/docs/ADAPTERS.md +76 -0
  15. data/docs/ADAPTIVE_SAMPLING.md +59 -0
  16. data/docs/COMPARISON.md +104 -0
  17. data/docs/CONFIGURATION.md +52 -0
  18. data/docs/DISTRIBUTED_TRACING.md +44 -0
  19. data/docs/LIMITATIONS.md +13 -0
  20. data/docs/METRICS_DSL.md +84 -0
  21. data/docs/PERFORMANCE.md +60 -0
  22. data/docs/PII_FILTERING.md +40 -0
  23. data/docs/PRESETS.md +65 -0
  24. data/docs/QUICK-START.md +546 -587
  25. data/docs/RAILS_INTEGRATION.md +79 -0
  26. data/docs/SCHEMA_VALIDATION.md +63 -0
  27. data/docs/SLO-PROMQL-ALERTS.md +161 -0
  28. data/docs/TESTING.md +69 -0
  29. data/docs/{ADR-001-architecture.md → architecture/ADR-001-architecture.md} +36 -65
  30. data/docs/{ADR-002-metrics-yabeda.md → architecture/ADR-002-metrics-yabeda.md} +62 -236
  31. data/docs/architecture/ADR-003-slo-observability.md +1402 -0
  32. data/docs/{ADR-004-adapter-architecture.md → architecture/ADR-004-adapter-architecture.md} +163 -146
  33. data/docs/{ADR-005-tracing-context.md → architecture/ADR-005-tracing-context.md} +10 -9
  34. data/docs/{ADR-006-security-compliance.md → architecture/ADR-006-security-compliance.md} +184 -191
  35. data/docs/{ADR-007-opentelemetry-integration.md → architecture/ADR-007-opentelemetry-integration.md} +3 -21
  36. data/docs/{ADR-008-rails-integration.md → architecture/ADR-008-rails-integration.md} +182 -743
  37. data/docs/{ADR-009-cost-optimization.md → architecture/ADR-009-cost-optimization.md} +45 -54
  38. data/docs/architecture/ADR-010-developer-experience.md +522 -0
  39. data/docs/{ADR-011-testing-strategy.md → architecture/ADR-011-testing-strategy.md} +44 -86
  40. data/docs/{ADR-012-event-evolution.md → architecture/ADR-012-event-evolution.md} +11 -11
  41. data/docs/{ADR-013-reliability-error-handling.md → architecture/ADR-013-reliability-error-handling.md} +37 -12
  42. data/docs/{ADR-014-event-driven-slo.md → architecture/ADR-014-event-driven-slo.md} +12 -24
  43. data/docs/{ADR-015-middleware-order.md → architecture/ADR-015-middleware-order.md} +43 -59
  44. data/docs/{ADR-016-self-monitoring-slo.md → architecture/ADR-016-self-monitoring-slo.md} +58 -355
  45. data/docs/{ADR-017-multi-rails-compatibility.md → architecture/ADR-017-multi-rails-compatibility.md} +4 -11
  46. data/docs/architecture/ADR-018-memory-optimization.md +366 -0
  47. data/docs/{ADR-INDEX.md → architecture/ADR-INDEX.md} +11 -6
  48. data/docs/plans/2026-03-20-browser-overlay-svelte.md +281 -0
  49. data/docs/{00-ICP-AND-TIMELINE.md → prd/00-ICP-AND-TIMELINE.md} +6 -6
  50. data/docs/{01-SCALE-REQUIREMENTS.md → prd/01-SCALE-REQUIREMENTS.md} +6 -6
  51. data/docs/prd/01-overview-vision.md +19 -14
  52. data/docs/use_cases/README.md +22 -23
  53. data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +50 -44
  54. data/docs/use_cases/UC-002-business-event-tracking.md +26 -95
  55. data/docs/use_cases/UC-003-event-metrics.md +66 -0
  56. data/docs/use_cases/UC-004-zero-config-slo-tracking.md +33 -684
  57. data/docs/use_cases/UC-005-sentry-integration.md +13 -15
  58. data/docs/use_cases/UC-006-trace-context-management.md +30 -28
  59. data/docs/use_cases/UC-007-pii-filtering.md +35 -87
  60. data/docs/use_cases/UC-008-opentelemetry-integration.md +51 -89
  61. data/docs/use_cases/UC-009-multi-service-tracing.md +30 -178
  62. data/docs/use_cases/UC-010-background-job-tracking.md +24 -91
  63. data/docs/use_cases/UC-011-rate-limiting.md +95 -168
  64. data/docs/use_cases/UC-012-audit-trail.md +21 -46
  65. data/docs/use_cases/UC-013-high-cardinality-protection.md +29 -167
  66. data/docs/use_cases/UC-014-adaptive-sampling.md +2 -2
  67. data/docs/use_cases/UC-015-cost-optimization.md +46 -99
  68. data/docs/use_cases/UC-016-rails-logger-migration.md +39 -213
  69. data/docs/use_cases/UC-017-local-development.md +203 -777
  70. data/docs/use_cases/UC-018-testing-events.md +3 -3
  71. data/docs/use_cases/UC-019-retention-based-routing.md +53 -106
  72. data/docs/use_cases/UC-020-event-versioning.md +8 -9
  73. data/docs/use_cases/UC-021-error-handling-retry-dlq.md +18 -22
  74. data/docs/use_cases/UC-022-event-registry.md +15 -21
  75. data/docs/use_cases/backlog.md +119 -87
  76. data/e11y.gemspec +2 -2
  77. data/gems/e11y-devtools/README.md +158 -0
  78. data/gems/e11y-devtools/config/routes.rb +15 -0
  79. data/gems/e11y-devtools/e11y-devtools.gemspec +25 -0
  80. data/gems/e11y-devtools/exe/e11y +34 -0
  81. data/gems/e11y-devtools/frontend/.gitignore +24 -0
  82. data/gems/e11y-devtools/frontend/README.md +51 -0
  83. data/gems/e11y-devtools/frontend/index.html +14 -0
  84. data/gems/e11y-devtools/frontend/package-lock.json +3707 -0
  85. data/gems/e11y-devtools/frontend/package.json +28 -0
  86. data/gems/e11y-devtools/frontend/public/mocks/v1/events/recent.json +4205 -0
  87. data/gems/e11y-devtools/frontend/public/mocks/v1/interactions.json +194 -0
  88. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/0a2e04027cfa22d014bc22e8b27cd913/events.json +86 -0
  89. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/0e1543af6a630fb3af6b52283154b3e0/events.json +169 -0
  90. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/1838b691faa49564f97db8592ff3978d/events.json +78 -0
  91. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/29f198f6588dacffb687777eb5f8f118/events.json +197 -0
  92. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/34bc3c9c0097de28a7a6f99b90a8e7bc/events.json +194 -0
  93. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/3ba6c20d068ab9cee00e51b180e66444/events.json +184 -0
  94. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/435bfd8f17b9009146a79812d7c3726d/events.json +144 -0
  95. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/4c7676e3fe668e99edb2b94d7d5678a9/events.json +222 -0
  96. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/6daf0d47974bedfc55d5de7004a3ea9f/events.json +194 -0
  97. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8a81ada42834d15f287bb40010043605/events.json +194 -0
  98. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8c0a98900edaae105469df8daedccf02/events.json +198 -0
  99. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/8e4f645180f8a7d1dce426b07380466b/events.json +222 -0
  100. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/93db346fa5d44a032605a13b627f4b80/events.json +128 -0
  101. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/98ff6146faf7bd9be8bd03a8275817ba/events.json +223 -0
  102. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/9997ddd0247bc7e25f2ca7a5c415c93d/events.json +197 -0
  103. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/99e35f8ef3baedd798cc4fd085980ad9/events.json +194 -0
  104. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/b4f3095c1909924cbc98889a86c83d6d/events.json +131 -0
  105. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/b54b7fc32b7575a7110de809d11ccda0/events.json +128 -0
  106. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/c0b48033fa06746bcc5886745e053cff/events.json +169 -0
  107. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/c44649ac76701b4558927cd2305ab535/events.json +169 -0
  108. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/d601ae3320057580a39dbdac2edfdf4a/events.json +248 -0
  109. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/e67e724bab422d2b52eeb49635e512e1/events.json +194 -0
  110. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/e6c72765a28f158a8485b35fa63f73da/events.json +194 -0
  111. data/gems/e11y-devtools/frontend/public/mocks/v1/traces/f541b87405c9a54819b18ebe529f6419/events.json +194 -0
  112. data/gems/e11y-devtools/frontend/scripts/generate_mocks.rb +397 -0
  113. data/gems/e11y-devtools/frontend/src/App.svelte +827 -0
  114. data/gems/e11y-devtools/frontend/src/components/Fab.svelte +19 -0
  115. data/gems/e11y-devtools/frontend/src/components/FilterBar.svelte +38 -0
  116. data/gems/e11y-devtools/frontend/src/components/FullscreenPanel.svelte +82 -0
  117. data/gems/e11y-devtools/frontend/src/components/InteractionsTimeline.svelte +264 -0
  118. data/gems/e11y-devtools/frontend/src/components/RecentHistogram.svelte +354 -0
  119. data/gems/e11y-devtools/frontend/src/lib/api.ts +37 -0
  120. data/gems/e11y-devtools/frontend/src/lib/eventIdentity.ts +12 -0
  121. data/gems/e11y-devtools/frontend/src/lib/format.ts +37 -0
  122. data/gems/e11y-devtools/frontend/src/lib/listFilter.ts +43 -0
  123. data/gems/e11y-devtools/frontend/src/lib/recentVolume.ts +80 -0
  124. data/gems/e11y-devtools/frontend/src/lib/router.ts +12 -0
  125. data/gems/e11y-devtools/frontend/src/lib/transitions.ts +34 -0
  126. data/gems/e11y-devtools/frontend/src/lib/viewportOrigin.ts +25 -0
  127. data/gems/e11y-devtools/frontend/src/main.ts +8 -0
  128. data/gems/e11y-devtools/frontend/src/overlay-entry.ts +24 -0
  129. data/gems/e11y-devtools/frontend/src/overlay.css +1080 -0
  130. data/gems/e11y-devtools/frontend/svelte.config.js +2 -0
  131. data/gems/e11y-devtools/frontend/test_puppeteer.js +41 -0
  132. data/gems/e11y-devtools/frontend/test_scale.js +3 -0
  133. data/gems/e11y-devtools/frontend/tsconfig.app.json +21 -0
  134. data/gems/e11y-devtools/frontend/tsconfig.json +7 -0
  135. data/gems/e11y-devtools/frontend/tsconfig.node.json +26 -0
  136. data/gems/e11y-devtools/frontend/vite.config.ts +36 -0
  137. data/gems/e11y-devtools/lib/e11y/devtools/mcp/server.rb +96 -0
  138. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tool_base.rb +25 -0
  139. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/clear.rb +31 -0
  140. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/errors.rb +35 -0
  141. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/event_detail.rb +33 -0
  142. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/events_by_trace.rb +33 -0
  143. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/interactions.rb +40 -0
  144. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/recent_events.rb +34 -0
  145. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/search.rb +34 -0
  146. data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/stats.rb +30 -0
  147. data/gems/e11y-devtools/lib/e11y/devtools/overlay/assets/overlay.js +20 -0
  148. data/gems/e11y-devtools/lib/e11y/devtools/overlay/controller.rb +94 -0
  149. data/gems/e11y-devtools/lib/e11y/devtools/overlay/engine.rb +26 -0
  150. data/gems/e11y-devtools/lib/e11y/devtools/overlay/middleware.rb +80 -0
  151. data/gems/e11y-devtools/lib/e11y/devtools/overlay/rails_controller.rb +67 -0
  152. data/gems/e11y-devtools/lib/e11y/devtools/tui/app.rb +262 -0
  153. data/gems/e11y-devtools/lib/e11y/devtools/tui/grouping.rb +66 -0
  154. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_detail.rb +62 -0
  155. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_list.rb +70 -0
  156. data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/interaction_list.rb +47 -0
  157. data/gems/e11y-devtools/lib/e11y/devtools/version.rb +8 -0
  158. data/gems/e11y-devtools/lib/e11y/devtools.rb +13 -0
  159. data/gems/e11y-devtools/spec/e11y/devtools/mcp/tools_spec.rb +107 -0
  160. data/gems/e11y-devtools/spec/e11y/devtools/overlay/controller_spec.rb +91 -0
  161. data/gems/e11y-devtools/spec/e11y/devtools/overlay/middleware_spec.rb +46 -0
  162. data/gems/e11y-devtools/spec/e11y/devtools/tui/app_spec.rb +85 -0
  163. data/gems/e11y-devtools/spec/e11y/devtools/tui/grouping_spec.rb +64 -0
  164. data/gems/e11y-devtools/spec/spec_helper.rb +5 -0
  165. data/gems/e11y-devtools/spec/tui/widgets/event_list_spec.rb +44 -0
  166. data/gems/e11y-devtools/spec/tui/widgets/interaction_list_spec.rb +62 -0
  167. data/lib/e11y/adapters/audit_encrypted.rb +53 -11
  168. data/lib/e11y/adapters/base.rb +33 -34
  169. data/lib/e11y/adapters/dev_log/file_store.rb +143 -0
  170. data/lib/e11y/adapters/dev_log/query.rb +219 -0
  171. data/lib/e11y/adapters/dev_log.rb +118 -0
  172. data/lib/e11y/adapters/file.rb +3 -6
  173. data/lib/e11y/adapters/in_memory.rb +52 -5
  174. data/lib/e11y/adapters/in_memory_test.rb +29 -0
  175. data/lib/e11y/adapters/loki.rb +58 -23
  176. data/lib/e11y/adapters/null.rb +82 -0
  177. data/lib/e11y/adapters/opentelemetry_collector.rb +183 -0
  178. data/lib/e11y/adapters/otel_logs.rb +136 -23
  179. data/lib/e11y/adapters/sentry.rb +4 -7
  180. data/lib/e11y/adapters/stdout.rb +73 -7
  181. data/lib/e11y/adapters/yabeda.rb +153 -29
  182. data/lib/e11y/buffers/adaptive_buffer.rb +3 -17
  183. data/lib/e11y/buffers/{request_scoped_buffer.rb → ephemeral_buffer.rb} +72 -58
  184. data/lib/e11y/buffers/ring_buffer.rb +3 -16
  185. data/lib/e11y/configuration.rb +272 -0
  186. data/lib/e11y/console.rb +10 -17
  187. data/lib/e11y/current.rb +53 -1
  188. data/lib/e11y/debug/pipeline_inspector.rb +96 -0
  189. data/lib/e11y/documentation/generator.rb +48 -0
  190. data/lib/e11y/event/base.rb +176 -82
  191. data/lib/e11y/event/value_sampling_config.rb +1 -5
  192. data/lib/e11y/events/rails/database/query.rb +1 -4
  193. data/lib/e11y/events/rails/job/failed.rb +2 -0
  194. data/lib/e11y/instruments/active_job.rb +44 -12
  195. data/lib/e11y/instruments/rails_instrumentation.rb +49 -24
  196. data/lib/e11y/instruments/sidekiq.rb +135 -31
  197. data/lib/e11y/linters/base.rb +11 -0
  198. data/lib/e11y/linters/pii/pii_declaration_linter.rb +120 -0
  199. data/lib/e11y/linters/slo/config_consistency_linter.rb +76 -0
  200. data/lib/e11y/linters/slo/explicit_declaration_linter.rb +36 -0
  201. data/lib/e11y/linters/slo/slo_status_from_linter.rb +41 -0
  202. data/lib/e11y/logger/bridge.rb +26 -7
  203. data/lib/e11y/metrics/cardinality_protection.rb +10 -15
  204. data/lib/e11y/metrics/cardinality_tracker.rb +16 -6
  205. data/lib/e11y/metrics/registry.rb +3 -5
  206. data/lib/e11y/metrics/test_backend.rb +62 -0
  207. data/lib/e11y/metrics.rb +56 -10
  208. data/lib/e11y/middleware/adapter_resolver.rb +40 -0
  209. data/lib/e11y/middleware/audit_signing.rb +43 -6
  210. data/lib/e11y/middleware/baggage_protection.rb +75 -0
  211. data/lib/e11y/middleware/dev_log_source.rb +24 -0
  212. data/lib/e11y/middleware/event_slo.rb +23 -9
  213. data/lib/e11y/middleware/otel_span.rb +23 -0
  214. data/lib/e11y/middleware/pii_filter.rb +104 -75
  215. data/lib/e11y/middleware/rate_limiting.rb +54 -27
  216. data/lib/e11y/middleware/request.rb +70 -23
  217. data/lib/e11y/middleware/routing.rb +78 -21
  218. data/lib/e11y/middleware/sampling.rb +66 -17
  219. data/lib/e11y/middleware/self_monitoring_emit.rb +39 -0
  220. data/lib/e11y/middleware/trace_context.rb +45 -10
  221. data/lib/e11y/middleware/track_latency.rb +34 -0
  222. data/lib/e11y/middleware/validation.rb +7 -16
  223. data/lib/e11y/middleware/versioning.rb +26 -22
  224. data/lib/e11y/opentelemetry/semantic_conventions.rb +109 -0
  225. data/lib/e11y/opentelemetry/span_creator.rb +142 -0
  226. data/lib/e11y/pii/patterns.rb +12 -1
  227. data/lib/e11y/pipeline/builder.rb +4 -4
  228. data/lib/e11y/presets/audit_event.rb +13 -2
  229. data/lib/e11y/railtie.rb +52 -14
  230. data/lib/e11y/registry.rb +306 -0
  231. data/lib/e11y/reliability/circuit_breaker.rb +19 -21
  232. data/lib/e11y/reliability/dlq/base.rb +71 -0
  233. data/lib/e11y/reliability/dlq/file_adapter.rb +301 -0
  234. data/lib/e11y/reliability/dlq/file_storage.rb +63 -34
  235. data/lib/e11y/reliability/dlq/filter.rb +37 -54
  236. data/lib/e11y/reliability/retry_handler.rb +26 -29
  237. data/lib/e11y/reliability/retry_rate_limiter.rb +3 -11
  238. data/lib/e11y/sampling/error_spike_detector.rb +0 -2
  239. data/lib/e11y/sampling/load_monitor.rb +5 -9
  240. data/lib/e11y/sampling/stratified_tracker.rb +18 -0
  241. data/lib/e11y/self_monitoring/buffer_monitor.rb +2 -0
  242. data/lib/e11y/self_monitoring/performance_monitor.rb +19 -61
  243. data/lib/e11y/self_monitoring/reliability_monitor.rb +4 -74
  244. data/lib/e11y/slo/config_loader.rb +40 -0
  245. data/lib/e11y/slo/config_validator.rb +58 -0
  246. data/lib/e11y/slo/dashboard_generator.rb +122 -0
  247. data/lib/e11y/slo/event_driven.rb +8 -0
  248. data/lib/e11y/slo/tracker.rb +31 -4
  249. data/lib/e11y/testing/have_tracked_event_matcher.rb +190 -0
  250. data/lib/e11y/testing/rspec_matchers.rb +21 -0
  251. data/lib/e11y/testing/snapshot_matcher.rb +86 -0
  252. data/lib/e11y/trace_context/sampler.rb +35 -0
  253. data/lib/e11y/tracing/faraday_middleware.rb +31 -0
  254. data/lib/e11y/tracing/net_http_patch.rb +33 -0
  255. data/lib/e11y/tracing/propagator.rb +144 -0
  256. data/lib/e11y/tracing.rb +47 -0
  257. data/lib/e11y/version.rb +1 -1
  258. data/lib/e11y/versioning/version_extractor.rb +32 -0
  259. data/lib/e11y.rb +123 -266
  260. data/lib/generators/e11y/event/event_generator.rb +22 -0
  261. data/lib/generators/e11y/event/templates/event.rb.tt +16 -0
  262. data/lib/generators/e11y/grafana_dashboard/grafana_dashboard_generator.rb +30 -0
  263. data/lib/generators/e11y/grafana_dashboard/templates/e11y_dashboard.json +81 -0
  264. data/lib/generators/e11y/install/install_generator.rb +34 -0
  265. data/lib/generators/e11y/install/templates/e11y.rb +239 -0
  266. data/lib/generators/e11y/prometheus_alerts/prometheus_alerts_generator.rb +29 -0
  267. data/lib/generators/e11y/prometheus_alerts/templates/e11y_alerts.yml +28 -0
  268. data/lib/tasks/e11y_docs.rake +30 -0
  269. data/lib/tasks/e11y_events.rake +71 -0
  270. data/lib/tasks/e11y_lint.rake +91 -0
  271. data/lib/tasks/e11y_slo.rake +29 -0
  272. metadata +186 -39
  273. data/docs/ADR-003-slo-observability.md +0 -3337
  274. data/docs/ADR-010-developer-experience.md +0 -2166
  275. data/docs/API-REFERENCE-L28.md +0 -914
  276. data/docs/COMPREHENSIVE-CONFIGURATION.md +0 -2366
  277. data/docs/CONTRIBUTING.md +0 -312
  278. data/docs/IMPLEMENTATION_NOTES.md +0 -2804
  279. data/docs/IMPLEMENTATION_PLAN.md +0 -1971
  280. data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +0 -586
  281. data/docs/PLAN.md +0 -148
  282. data/docs/README.md +0 -296
  283. data/docs/design/00-memory-optimization.md +0 -593
  284. data/docs/guides/MIGRATION-L27-L28.md +0 -692
  285. data/docs/guides/PERFORMANCE-BENCHMARKS.md +0 -434
  286. data/docs/guides/README.md +0 -44
  287. data/docs/use_cases/UC-003-pattern-based-metrics.md +0 -1627
  288. 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 (safe keys that don't contain PII)
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>] Allowlist of safe baggage keys
86
- # @param max_attributes [Integer] Max attributes per log (cardinality protection)
87
- def initialize(service_name: nil, baggage_allowlist: DEFAULT_BAGGAGE_ALLOWLIST, max_attributes: 50, **)
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
- log_record = build_log_record(event_data)
102
- @logger.emit_log_record(log_record)
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
- @logger_provider = OpenTelemetry::SDK::Logs::LoggerProvider.new
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 OTel log record from E11y event
225
+ # Build params for Logger#on_emit from E11y event
140
226
  #
141
227
  # @param event_data [Hash] E11y event payload
142
- # @return [OpenTelemetry::SDK::Logs::LogRecord] OTel log record
143
- def build_log_record(event_data)
144
- OpenTelemetry::SDK::Logs::LogRecord.new(
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
- # - Baggage PII filtering (C08 Resolution)
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
- # C08: Baggage PII protection - only allowlisted keys
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
- attributes["event.#{key}"] = value
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 (C08 Resolution)
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] true if key is in allowlist
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
@@ -33,11 +33,8 @@ module E11y
33
33
  # severity_threshold: :warn
34
34
  # )
35
35
  #
36
- # @example With Registry
37
- # E11y::Adapters::Registry.register(
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, Metrics/MethodLength
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, Metrics/MethodLength
187
+ # rubocop:enable Metrics/AbcSize
191
188
 
192
189
  # Send breadcrumb to Sentry
193
190
  #
@@ -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
- @pretty_print = config.fetch(:pretty_print, true)
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
- if @pretty_print
91
- JSON.pretty_generate(event_data)
92
- else
93
- event_data.to_json
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