e11y 0.2.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +130 -10
- data/CHANGELOG.md +56 -1
- data/CLAUDE.md +168 -0
- data/CONTRIBUTING.md +640 -0
- data/README.md +134 -702
- data/RELEASE.md +18 -3
- data/Rakefile +108 -29
- data/config/README.md +1 -1
- data/config/loki-local-config.yaml +12 -0
- data/config/otel-collector-config.yaml +44 -0
- data/cucumber.yml +1 -0
- data/docker-compose.yml +18 -2
- data/docs/ADAPTERS.md +76 -0
- data/docs/ADAPTIVE_SAMPLING.md +59 -0
- data/docs/COMPARISON.md +104 -0
- data/docs/CONFIGURATION.md +52 -0
- data/docs/DISTRIBUTED_TRACING.md +44 -0
- data/docs/LIMITATIONS.md +13 -0
- data/docs/METRICS_DSL.md +84 -0
- data/docs/PERFORMANCE.md +60 -0
- data/docs/PII_FILTERING.md +40 -0
- data/docs/PRESETS.md +65 -0
- data/docs/QUICK-START.md +546 -587
- data/docs/RAILS_INTEGRATION.md +29 -0
- data/docs/SCHEMA_VALIDATION.md +63 -0
- data/docs/SLO-PROMQL-ALERTS.md +161 -0
- data/docs/TESTING.md +69 -0
- data/docs/{ADR-001-architecture.md → architecture/ADR-001-architecture.md} +35 -64
- data/docs/{ADR-002-metrics-yabeda.md → architecture/ADR-002-metrics-yabeda.md} +62 -236
- data/docs/{ADR-003-slo-observability.md → architecture/ADR-003-slo-observability.md} +27 -466
- data/docs/{ADR-004-adapter-architecture.md → architecture/ADR-004-adapter-architecture.md} +163 -146
- data/docs/{ADR-005-tracing-context.md → architecture/ADR-005-tracing-context.md} +10 -9
- data/docs/{ADR-006-security-compliance.md → architecture/ADR-006-security-compliance.md} +184 -191
- data/docs/{ADR-007-opentelemetry-integration.md → architecture/ADR-007-opentelemetry-integration.md} +3 -21
- data/docs/{ADR-008-rails-integration.md → architecture/ADR-008-rails-integration.md} +209 -339
- data/docs/{ADR-009-cost-optimization.md → architecture/ADR-009-cost-optimization.md} +45 -54
- data/docs/architecture/ADR-010-developer-experience.md +522 -0
- data/docs/{ADR-011-testing-strategy.md → architecture/ADR-011-testing-strategy.md} +41 -83
- data/docs/{ADR-013-reliability-error-handling.md → architecture/ADR-013-reliability-error-handling.md} +37 -12
- data/docs/{ADR-014-event-driven-slo.md → architecture/ADR-014-event-driven-slo.md} +12 -24
- data/docs/{ADR-015-middleware-order.md → architecture/ADR-015-middleware-order.md} +23 -41
- data/docs/{ADR-016-self-monitoring-slo.md → architecture/ADR-016-self-monitoring-slo.md} +52 -349
- data/docs/{ADR-017-multi-rails-compatibility.md → architecture/ADR-017-multi-rails-compatibility.md} +4 -11
- data/docs/architecture/ADR-018-memory-optimization.md +366 -0
- data/docs/{ADR-INDEX.md → architecture/ADR-INDEX.md} +11 -6
- data/docs/{00-ICP-AND-TIMELINE.md → prd/00-ICP-AND-TIMELINE.md} +6 -6
- data/docs/{01-SCALE-REQUIREMENTS.md → prd/01-SCALE-REQUIREMENTS.md} +6 -6
- data/docs/prd/01-overview-vision.md +19 -14
- data/docs/use_cases/README.md +22 -23
- data/docs/use_cases/UC-001-request-scoped-debug-buffering.md +50 -44
- data/docs/use_cases/UC-002-business-event-tracking.md +26 -95
- data/docs/use_cases/UC-003-event-metrics.md +66 -0
- data/docs/use_cases/UC-004-zero-config-slo-tracking.md +42 -101
- data/docs/use_cases/UC-005-sentry-integration.md +13 -15
- data/docs/use_cases/UC-006-trace-context-management.md +30 -28
- data/docs/use_cases/UC-007-pii-filtering.md +35 -87
- data/docs/use_cases/UC-008-opentelemetry-integration.md +51 -89
- data/docs/use_cases/UC-009-multi-service-tracing.md +4 -4
- data/docs/use_cases/UC-010-background-job-tracking.md +5 -5
- data/docs/use_cases/UC-011-rate-limiting.md +95 -168
- data/docs/use_cases/UC-012-audit-trail.md +21 -46
- data/docs/use_cases/UC-013-high-cardinality-protection.md +29 -167
- data/docs/use_cases/UC-014-adaptive-sampling.md +2 -2
- data/docs/use_cases/UC-015-cost-optimization.md +46 -99
- data/docs/use_cases/UC-016-rails-logger-migration.md +39 -213
- data/docs/use_cases/UC-017-local-development.md +203 -777
- data/docs/use_cases/UC-018-testing-events.md +3 -3
- data/docs/use_cases/UC-019-retention-based-routing.md +53 -106
- data/docs/use_cases/UC-020-event-versioning.md +8 -9
- data/docs/use_cases/UC-021-error-handling-retry-dlq.md +18 -22
- data/docs/use_cases/UC-022-event-registry.md +15 -21
- data/docs/use_cases/backlog.md +119 -87
- data/e11y.gemspec +2 -2
- data/gems/e11y-devtools/README.md +136 -0
- data/gems/e11y-devtools/config/routes.rb +8 -0
- data/gems/e11y-devtools/e11y-devtools.gemspec +25 -0
- data/gems/e11y-devtools/exe/e11y +34 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/server.rb +96 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tool_base.rb +25 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/clear.rb +31 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/errors.rb +35 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/event_detail.rb +33 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/events_by_trace.rb +33 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/interactions.rb +40 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/recent_events.rb +34 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/search.rb +34 -0
- data/gems/e11y-devtools/lib/e11y/devtools/mcp/tools/stats.rb +30 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/assets/overlay.js +115 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/controller.rb +54 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/engine.rb +26 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/middleware.rb +80 -0
- data/gems/e11y-devtools/lib/e11y/devtools/overlay/rails_controller.rb +42 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/app.rb +262 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/grouping.rb +66 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_detail.rb +62 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/event_list.rb +70 -0
- data/gems/e11y-devtools/lib/e11y/devtools/tui/widgets/interaction_list.rb +47 -0
- data/gems/e11y-devtools/lib/e11y/devtools/version.rb +8 -0
- data/gems/e11y-devtools/lib/e11y/devtools.rb +13 -0
- data/gems/e11y-devtools/spec/e11y/devtools/mcp/tools_spec.rb +107 -0
- data/gems/e11y-devtools/spec/e11y/devtools/overlay/controller_spec.rb +58 -0
- data/gems/e11y-devtools/spec/e11y/devtools/overlay/middleware_spec.rb +46 -0
- data/gems/e11y-devtools/spec/e11y/devtools/tui/app_spec.rb +85 -0
- data/gems/e11y-devtools/spec/e11y/devtools/tui/grouping_spec.rb +64 -0
- data/gems/e11y-devtools/spec/spec_helper.rb +5 -0
- data/gems/e11y-devtools/spec/tui/widgets/event_list_spec.rb +44 -0
- data/gems/e11y-devtools/spec/tui/widgets/interaction_list_spec.rb +62 -0
- data/lib/e11y/adapters/audit_encrypted.rb +53 -11
- data/lib/e11y/adapters/base.rb +33 -34
- data/lib/e11y/adapters/dev_log/file_store.rb +143 -0
- data/lib/e11y/adapters/dev_log/query.rb +219 -0
- data/lib/e11y/adapters/dev_log.rb +118 -0
- data/lib/e11y/adapters/file.rb +3 -6
- data/lib/e11y/adapters/in_memory.rb +52 -5
- data/lib/e11y/adapters/in_memory_test.rb +29 -0
- data/lib/e11y/adapters/loki.rb +58 -23
- data/lib/e11y/adapters/null.rb +82 -0
- data/lib/e11y/adapters/opentelemetry_collector.rb +183 -0
- data/lib/e11y/adapters/otel_logs.rb +136 -23
- data/lib/e11y/adapters/sentry.rb +4 -7
- data/lib/e11y/adapters/stdout.rb +73 -7
- data/lib/e11y/adapters/yabeda.rb +153 -29
- data/lib/e11y/buffers/adaptive_buffer.rb +3 -17
- data/lib/e11y/buffers/{request_scoped_buffer.rb → ephemeral_buffer.rb} +72 -58
- data/lib/e11y/buffers/ring_buffer.rb +3 -16
- data/lib/e11y/configuration.rb +272 -0
- data/lib/e11y/console.rb +10 -17
- data/lib/e11y/current.rb +53 -1
- data/lib/e11y/debug/pipeline_inspector.rb +96 -0
- data/lib/e11y/documentation/generator.rb +48 -0
- data/lib/e11y/event/base.rb +176 -82
- data/lib/e11y/event/value_sampling_config.rb +1 -5
- data/lib/e11y/events/rails/database/query.rb +1 -4
- data/lib/e11y/events/rails/job/failed.rb +2 -0
- data/lib/e11y/instruments/active_job.rb +46 -12
- data/lib/e11y/instruments/rails_instrumentation.rb +49 -24
- data/lib/e11y/instruments/sidekiq.rb +137 -31
- data/lib/e11y/linters/base.rb +11 -0
- data/lib/e11y/linters/pii/pii_declaration_linter.rb +120 -0
- data/lib/e11y/linters/slo/config_consistency_linter.rb +76 -0
- data/lib/e11y/linters/slo/explicit_declaration_linter.rb +36 -0
- data/lib/e11y/linters/slo/slo_status_from_linter.rb +41 -0
- data/lib/e11y/logger/bridge.rb +26 -7
- data/lib/e11y/metrics/cardinality_protection.rb +10 -15
- data/lib/e11y/metrics/cardinality_tracker.rb +16 -6
- data/lib/e11y/metrics/registry.rb +3 -5
- data/lib/e11y/metrics/test_backend.rb +62 -0
- data/lib/e11y/metrics.rb +56 -10
- data/lib/e11y/middleware/adapter_resolver.rb +40 -0
- data/lib/e11y/middleware/audit_signing.rb +43 -6
- data/lib/e11y/middleware/baggage_protection.rb +75 -0
- data/lib/e11y/middleware/dev_log_source.rb +24 -0
- data/lib/e11y/middleware/event_slo.rb +23 -9
- data/lib/e11y/middleware/otel_span.rb +23 -0
- data/lib/e11y/middleware/pii_filter.rb +104 -75
- data/lib/e11y/middleware/rate_limiting.rb +54 -27
- data/lib/e11y/middleware/request.rb +70 -23
- data/lib/e11y/middleware/routing.rb +78 -21
- data/lib/e11y/middleware/sampling.rb +66 -17
- data/lib/e11y/middleware/self_monitoring_emit.rb +39 -0
- data/lib/e11y/middleware/trace_context.rb +45 -10
- data/lib/e11y/middleware/track_latency.rb +34 -0
- data/lib/e11y/middleware/validation.rb +7 -16
- data/lib/e11y/middleware/versioning.rb +26 -22
- data/lib/e11y/opentelemetry/semantic_conventions.rb +109 -0
- data/lib/e11y/opentelemetry/span_creator.rb +142 -0
- data/lib/e11y/pii/patterns.rb +12 -1
- data/lib/e11y/pipeline/builder.rb +1 -1
- data/lib/e11y/presets/audit_event.rb +13 -2
- data/lib/e11y/railtie.rb +52 -15
- data/lib/e11y/registry.rb +306 -0
- data/lib/e11y/reliability/circuit_breaker.rb +19 -21
- data/lib/e11y/reliability/dlq/base.rb +71 -0
- data/lib/e11y/reliability/dlq/file_adapter.rb +301 -0
- data/lib/e11y/reliability/dlq/file_storage.rb +63 -34
- data/lib/e11y/reliability/dlq/filter.rb +37 -54
- data/lib/e11y/reliability/retry_handler.rb +26 -29
- data/lib/e11y/reliability/retry_rate_limiter.rb +3 -11
- data/lib/e11y/sampling/error_spike_detector.rb +0 -2
- data/lib/e11y/sampling/load_monitor.rb +5 -9
- data/lib/e11y/sampling/stratified_tracker.rb +18 -0
- data/lib/e11y/self_monitoring/buffer_monitor.rb +2 -0
- data/lib/e11y/self_monitoring/performance_monitor.rb +19 -61
- data/lib/e11y/self_monitoring/reliability_monitor.rb +4 -74
- data/lib/e11y/slo/config_loader.rb +40 -0
- data/lib/e11y/slo/config_validator.rb +58 -0
- data/lib/e11y/slo/dashboard_generator.rb +122 -0
- data/lib/e11y/slo/event_driven.rb +8 -0
- data/lib/e11y/slo/tracker.rb +31 -4
- data/lib/e11y/testing/have_tracked_event_matcher.rb +190 -0
- data/lib/e11y/testing/rspec_matchers.rb +21 -0
- data/lib/e11y/testing/snapshot_matcher.rb +86 -0
- data/lib/e11y/trace_context/sampler.rb +35 -0
- data/lib/e11y/tracing/faraday_middleware.rb +31 -0
- data/lib/e11y/tracing/net_http_patch.rb +33 -0
- data/lib/e11y/tracing/propagator.rb +116 -0
- data/lib/e11y/tracing.rb +47 -0
- data/lib/e11y/version.rb +1 -1
- data/lib/e11y/versioning/version_extractor.rb +32 -0
- data/lib/e11y.rb +141 -265
- data/lib/generators/e11y/event/event_generator.rb +22 -0
- data/lib/generators/e11y/event/templates/event.rb.tt +16 -0
- data/lib/generators/e11y/grafana_dashboard/grafana_dashboard_generator.rb +30 -0
- data/lib/generators/e11y/grafana_dashboard/templates/e11y_dashboard.json +81 -0
- data/lib/generators/e11y/install/install_generator.rb +34 -0
- data/lib/generators/e11y/install/templates/e11y.rb +239 -0
- data/lib/generators/e11y/prometheus_alerts/prometheus_alerts_generator.rb +29 -0
- data/lib/generators/e11y/prometheus_alerts/templates/e11y_alerts.yml +28 -0
- data/lib/tasks/e11y_docs.rake +30 -0
- data/lib/tasks/e11y_events.rake +71 -0
- data/lib/tasks/e11y_lint.rake +91 -0
- data/lib/tasks/e11y_slo.rake +29 -0
- metadata +129 -39
- data/docs/ADR-010-developer-experience.md +0 -2166
- data/docs/API-REFERENCE-L28.md +0 -914
- data/docs/COMPREHENSIVE-CONFIGURATION.md +0 -2366
- data/docs/CONTRIBUTING.md +0 -312
- data/docs/IMPLEMENTATION_NOTES.md +0 -2804
- data/docs/IMPLEMENTATION_PLAN.md +0 -1971
- data/docs/IMPLEMENTATION_PLAN_ARCHITECTURE.md +0 -586
- data/docs/PLAN.md +0 -148
- data/docs/README.md +0 -296
- data/docs/design/00-memory-optimization.md +0 -593
- data/docs/guides/MIGRATION-L27-L28.md +0 -692
- data/docs/guides/PERFORMANCE-BENCHMARKS.md +0 -434
- data/docs/guides/README.md +0 -44
- data/docs/use_cases/UC-003-pattern-based-metrics.md +0 -1627
- data/lib/e11y/adapters/registry.rb +0 -141
- /data/docs/{ADR-012-event-evolution.md → architecture/ADR-012-event-evolution.md} +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "pathname"
|
|
5
|
+
require "e11y/adapters/dev_log/query"
|
|
6
|
+
require_relative "grouping"
|
|
7
|
+
|
|
8
|
+
module E11y
|
|
9
|
+
module Devtools
|
|
10
|
+
module Tui
|
|
11
|
+
# Top-level TUI application.
|
|
12
|
+
#
|
|
13
|
+
# Manages navigation state (:interactions | :events | :detail),
|
|
14
|
+
# handles keyboard events, and reloads data when the log file changes.
|
|
15
|
+
# rubocop:disable Metrics/ClassLength
|
|
16
|
+
class App
|
|
17
|
+
attr_reader :current_view, :source_filter
|
|
18
|
+
|
|
19
|
+
POLL_INTERVAL_MS = 250
|
|
20
|
+
|
|
21
|
+
def initialize(log_path: nil)
|
|
22
|
+
@log_path = log_path || auto_detect_log_path
|
|
23
|
+
@query = E11y::Adapters::DevLog::Query.new(@log_path)
|
|
24
|
+
@current_view = :interactions
|
|
25
|
+
@source_filter = :web
|
|
26
|
+
@selected_ix = 0
|
|
27
|
+
@interactions = []
|
|
28
|
+
@events = []
|
|
29
|
+
@current_trace_id = nil
|
|
30
|
+
@current_event = nil
|
|
31
|
+
@last_mtime = nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Start the TUI event loop (blocks until user quits).
|
|
35
|
+
def run
|
|
36
|
+
require "ratatui_ruby"
|
|
37
|
+
require_relative "widgets/interaction_list"
|
|
38
|
+
require_relative "widgets/event_list"
|
|
39
|
+
require_relative "widgets/event_detail"
|
|
40
|
+
RatatuiRuby.run do |tui|
|
|
41
|
+
loop do
|
|
42
|
+
reload_if_changed!
|
|
43
|
+
tui.draw { |frame| render(tui, frame) }
|
|
44
|
+
event = tui.poll_event(timeout_ms: POLL_INTERVAL_MS)
|
|
45
|
+
break if quit_event?(event)
|
|
46
|
+
|
|
47
|
+
handle_key(key_from(event)) if key_event?(event)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Handle a single key press (public for testability).
|
|
53
|
+
def handle_key(key)
|
|
54
|
+
case @current_view
|
|
55
|
+
when :interactions then handle_interactions_key(key)
|
|
56
|
+
when :events then handle_events_key(key)
|
|
57
|
+
when :detail then handle_detail_key(key)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Return the currently highlighted interaction (or nil).
|
|
62
|
+
def selected_interaction
|
|
63
|
+
@interactions[@selected_ix]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
private
|
|
67
|
+
|
|
68
|
+
# --- Rendering ---
|
|
69
|
+
|
|
70
|
+
def render(tui, frame)
|
|
71
|
+
case @current_view
|
|
72
|
+
when :interactions then render_interactions(tui, frame)
|
|
73
|
+
when :events then render_events(tui, frame)
|
|
74
|
+
when :detail
|
|
75
|
+
render_events(tui, frame)
|
|
76
|
+
Widgets::EventDetail.new(event: @current_event).render(tui, frame, frame.area)
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def render_interactions(tui, frame)
|
|
81
|
+
Widgets::InteractionList.new(
|
|
82
|
+
interactions: @interactions,
|
|
83
|
+
selected_index: @selected_ix
|
|
84
|
+
).render(tui, frame, frame.area)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def render_events(tui, frame)
|
|
88
|
+
Widgets::EventList.new(
|
|
89
|
+
events: @events,
|
|
90
|
+
trace_id: @current_trace_id || "",
|
|
91
|
+
selected_index: @selected_ix
|
|
92
|
+
).render(tui, frame, frame.area)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# --- Key handlers per view ---
|
|
96
|
+
|
|
97
|
+
def handle_interactions_key(key)
|
|
98
|
+
case key
|
|
99
|
+
when "enter" then drill_into_events
|
|
100
|
+
when "j" then @source_filter = :job
|
|
101
|
+
reload!
|
|
102
|
+
when "w" then @source_filter = :web
|
|
103
|
+
reload!
|
|
104
|
+
when "a" then @source_filter = :all
|
|
105
|
+
reload!
|
|
106
|
+
when "down" then move_down(@interactions.size)
|
|
107
|
+
when "up" then move_up
|
|
108
|
+
when "r" then reload!
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def handle_events_key(key)
|
|
113
|
+
case key
|
|
114
|
+
when "esc", "b" then back_to_interactions
|
|
115
|
+
when "enter" then drill_into_detail
|
|
116
|
+
when "down" then move_down(@events.size)
|
|
117
|
+
when "up" then move_up
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def handle_detail_key(key)
|
|
122
|
+
case key
|
|
123
|
+
when "esc", "b" then @current_view = :events
|
|
124
|
+
when "c" then copy_to_clipboard(::JSON.generate(@current_event))
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# --- Navigation helpers ---
|
|
129
|
+
|
|
130
|
+
def drill_into_events
|
|
131
|
+
ix = selected_interaction
|
|
132
|
+
return unless ix
|
|
133
|
+
|
|
134
|
+
@current_trace_id = ix.trace_ids.first
|
|
135
|
+
@events = @query.events_by_trace(@current_trace_id)
|
|
136
|
+
@selected_ix = 0
|
|
137
|
+
@current_view = :events
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def drill_into_detail
|
|
141
|
+
event = @events[@selected_ix]
|
|
142
|
+
return unless event
|
|
143
|
+
|
|
144
|
+
@current_event = event
|
|
145
|
+
@current_view = :detail
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def back_to_interactions
|
|
149
|
+
@current_view = :interactions
|
|
150
|
+
@selected_ix = 0
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def move_down(size)
|
|
154
|
+
@selected_ix = [@selected_ix + 1, size - 1].min
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def move_up
|
|
158
|
+
@selected_ix = [@selected_ix - 1, 0].max
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# --- Data loading ---
|
|
162
|
+
|
|
163
|
+
def reload_if_changed!
|
|
164
|
+
mtime = file_mtime
|
|
165
|
+
return if mtime == @last_mtime
|
|
166
|
+
|
|
167
|
+
@last_mtime = mtime
|
|
168
|
+
reload!
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def reload!
|
|
172
|
+
source = @source_filter == :all ? nil : @source_filter.to_s
|
|
173
|
+
traces = build_traces(source)
|
|
174
|
+
@interactions = Grouping.group(traces, window_ms: 500)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def build_traces(source)
|
|
178
|
+
events = @query.stored_events(limit: 5000, source: source)
|
|
179
|
+
trace_map = {}
|
|
180
|
+
events.each { |e| accumulate_trace(trace_map, e) }
|
|
181
|
+
trace_map.values
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def accumulate_trace(trace_map, event)
|
|
185
|
+
tid = event["trace_id"]
|
|
186
|
+
return unless tid
|
|
187
|
+
|
|
188
|
+
entry = trace_map[tid] ||= {
|
|
189
|
+
trace_id: tid,
|
|
190
|
+
started_at: parse_time(event.dig("metadata", "started_at") || event["timestamp"]),
|
|
191
|
+
severity: event["severity"],
|
|
192
|
+
source: event.dig("metadata", "source") || "web"
|
|
193
|
+
}
|
|
194
|
+
entry[:severity] = "error" if %w[error fatal].include?(event["severity"])
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def file_mtime
|
|
198
|
+
::File.mtime(@log_path)
|
|
199
|
+
rescue Errno::ENOENT
|
|
200
|
+
nil
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# --- Utilities ---
|
|
204
|
+
|
|
205
|
+
def auto_detect_log_path
|
|
206
|
+
dir = Pathname.new(Dir.pwd)
|
|
207
|
+
loop do
|
|
208
|
+
candidate = dir.join("log", "e11y_dev.jsonl")
|
|
209
|
+
return candidate.to_s if candidate.exist?
|
|
210
|
+
|
|
211
|
+
parent = dir.parent
|
|
212
|
+
break if parent == dir
|
|
213
|
+
|
|
214
|
+
dir = parent
|
|
215
|
+
end
|
|
216
|
+
"log/e11y_dev.jsonl"
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def parse_time(str)
|
|
220
|
+
::Time.parse(str.to_s)
|
|
221
|
+
rescue ArgumentError, TypeError
|
|
222
|
+
::Time.now
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def quit_event?(event)
|
|
226
|
+
return false unless event
|
|
227
|
+
return false unless event[:type] == :key
|
|
228
|
+
|
|
229
|
+
event[:code] == "q" ||
|
|
230
|
+
(event[:code] == "c" && event[:modifiers]&.include?("ctrl"))
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def key_event?(event)
|
|
234
|
+
event && event[:type] == :key
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def key_from(event)
|
|
238
|
+
event&.dig(:code)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def copy_to_clipboard(text)
|
|
242
|
+
copy_macos(text) || copy_linux(text)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def copy_macos(text)
|
|
246
|
+
::IO.popen("pbcopy", "w") { |f| f.write(text) }
|
|
247
|
+
true
|
|
248
|
+
rescue StandardError
|
|
249
|
+
false
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def copy_linux(text)
|
|
253
|
+
::IO.popen("xclip -selection clipboard", "w") { |f| f.write(text) }
|
|
254
|
+
true
|
|
255
|
+
rescue StandardError
|
|
256
|
+
false
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
# rubocop:enable Metrics/ClassLength
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Devtools
|
|
5
|
+
module Tui
|
|
6
|
+
# Pure-function time-window grouping for traces → interactions.
|
|
7
|
+
# Shared by TUI widgets, Overlay, and MCP interactions tool.
|
|
8
|
+
module Grouping
|
|
9
|
+
# Severities that count as errors for interaction flagging.
|
|
10
|
+
ERROR_SEVERITIES = %w[error fatal].freeze
|
|
11
|
+
|
|
12
|
+
# Value object representing one interaction group.
|
|
13
|
+
Interaction = Struct.new(:started_at, :trace_ids, :has_error?,
|
|
14
|
+
:source)
|
|
15
|
+
|
|
16
|
+
# Group an array of trace hashes into Interaction bands.
|
|
17
|
+
#
|
|
18
|
+
# @param traces [Array<Hash>] Each hash must have :trace_id,
|
|
19
|
+
# :started_at (Time), :severity
|
|
20
|
+
# @param window_ms [Integer] Grouping window in milliseconds
|
|
21
|
+
# @return [Array<Interaction>] Newest-first
|
|
22
|
+
def self.group(traces, window_ms: 500)
|
|
23
|
+
return [] if traces.empty?
|
|
24
|
+
|
|
25
|
+
build_interactions(accumulate_groups(traces, window_ms))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.accumulate_groups(traces, window_ms)
|
|
29
|
+
sorted = traces.sort_by { |t| t[:started_at] }
|
|
30
|
+
groups = []
|
|
31
|
+
current = nil
|
|
32
|
+
sorted.each { |trace| current = append_trace(groups, current, trace, window_ms) }
|
|
33
|
+
groups
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.append_trace(groups, current, trace, window_ms)
|
|
37
|
+
if current.nil? || outside_window?(trace, current, window_ms)
|
|
38
|
+
current = new_group(trace)
|
|
39
|
+
groups << current
|
|
40
|
+
end
|
|
41
|
+
current[:trace_ids] << trace[:trace_id]
|
|
42
|
+
current[:has_error] ||= ERROR_SEVERITIES.include?(trace[:severity])
|
|
43
|
+
current
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def self.outside_window?(trace, current, window_ms)
|
|
47
|
+
(trace[:started_at] - current[:anchor]) * 1000 > window_ms
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.new_group(trace)
|
|
51
|
+
{ anchor: trace[:started_at], started_at: trace[:started_at],
|
|
52
|
+
trace_ids: [], has_error: false, source: trace[:source] }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.build_interactions(groups)
|
|
56
|
+
groups.reverse.map do |g|
|
|
57
|
+
Interaction.new(g[:started_at], g[:trace_ids], g[:has_error], g[:source])
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private_class_method :accumulate_groups, :append_trace,
|
|
62
|
+
:outside_window?, :new_group, :build_interactions
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ratatui_ruby"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module E11y
|
|
7
|
+
module Devtools
|
|
8
|
+
module Tui
|
|
9
|
+
module Widgets
|
|
10
|
+
# Full-screen popup overlay showing event payload + metadata.
|
|
11
|
+
class EventDetail
|
|
12
|
+
def initialize(event:)
|
|
13
|
+
@event = event
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render(tui, frame, area)
|
|
17
|
+
popup_area = centered_rect(area, percent_x: 80, percent_y: 70)
|
|
18
|
+
|
|
19
|
+
frame.render_widget(tui.clear, popup_area)
|
|
20
|
+
|
|
21
|
+
sev = @event["severity"] || "info"
|
|
22
|
+
title = " #{@event['event_name']} · #{sev.upcase} "
|
|
23
|
+
|
|
24
|
+
frame.render_widget(
|
|
25
|
+
tui.paragraph(
|
|
26
|
+
text: build_lines,
|
|
27
|
+
block: tui.block(title: title, borders: :all),
|
|
28
|
+
scroll: [0, 0]
|
|
29
|
+
),
|
|
30
|
+
popup_area
|
|
31
|
+
)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def build_lines
|
|
37
|
+
lines = []
|
|
38
|
+
lines << " timestamp: #{@event['timestamp']}"
|
|
39
|
+
lines << " trace_id: #{@event['trace_id']}"
|
|
40
|
+
lines << " span_id: #{@event['span_id']}"
|
|
41
|
+
lines << ""
|
|
42
|
+
lines << " payload:"
|
|
43
|
+
JSON.pretty_generate(@event["payload"] || {}).each_line do |l|
|
|
44
|
+
lines << " #{l.chomp}"
|
|
45
|
+
end
|
|
46
|
+
lines << ""
|
|
47
|
+
lines << " [c] copy JSON [b] back"
|
|
48
|
+
lines
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def centered_rect(area, percent_x:, percent_y:)
|
|
52
|
+
w = (area.width * percent_x / 100).to_i
|
|
53
|
+
h = (area.height * percent_y / 100).to_i
|
|
54
|
+
x = area.x + ((area.width - w) / 2)
|
|
55
|
+
y = area.y + ((area.height - h) / 2)
|
|
56
|
+
RatatuiRuby::Rect.new(x: x, y: y, width: w, height: h)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ratatui_ruby"
|
|
4
|
+
|
|
5
|
+
module E11y
|
|
6
|
+
module Devtools
|
|
7
|
+
module Tui
|
|
8
|
+
module Widgets
|
|
9
|
+
# Renders a table of events for the selected trace.
|
|
10
|
+
class EventList
|
|
11
|
+
SEVERITY_COLORS = {
|
|
12
|
+
"debug" => :dark_gray,
|
|
13
|
+
"info" => :white,
|
|
14
|
+
"warn" => :yellow,
|
|
15
|
+
"error" => :red,
|
|
16
|
+
"fatal" => :red
|
|
17
|
+
}.freeze
|
|
18
|
+
|
|
19
|
+
def initialize(events:, trace_id:, selected_index: 0)
|
|
20
|
+
@events = events
|
|
21
|
+
@trace_id = trace_id
|
|
22
|
+
@selected_index = selected_index
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render(tui, frame, area)
|
|
26
|
+
frame.render_widget(
|
|
27
|
+
tui.table(
|
|
28
|
+
header: ["#", "Severity", "Event Name", "Duration", "At"],
|
|
29
|
+
rows: build_rows(tui),
|
|
30
|
+
row_highlight_style: { bg: :dark_gray },
|
|
31
|
+
selected_row: @selected_index,
|
|
32
|
+
block: tui.block(title: " #{@trace_id} ", borders: :all)
|
|
33
|
+
),
|
|
34
|
+
area
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def build_rows(tui)
|
|
41
|
+
@events.each_with_index.map do |e, i|
|
|
42
|
+
sev = e["severity"] || "info"
|
|
43
|
+
color = SEVERITY_COLORS.fetch(sev, :white)
|
|
44
|
+
[
|
|
45
|
+
(i + 1).to_s,
|
|
46
|
+
tui.span(content: sev.upcase, style: { fg: color }),
|
|
47
|
+
e["event_name"].to_s,
|
|
48
|
+
duration_str(e),
|
|
49
|
+
timestamp_short(e["timestamp"])
|
|
50
|
+
]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def duration_str(event)
|
|
55
|
+
ms = event.dig("metadata", "duration_ms")
|
|
56
|
+
ms ? "#{ms}ms" : "—"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def timestamp_short(timestamp)
|
|
60
|
+
return "—" unless timestamp
|
|
61
|
+
|
|
62
|
+
Time.parse(timestamp).strftime(".%L")
|
|
63
|
+
rescue ArgumentError
|
|
64
|
+
"—"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ratatui_ruby"
|
|
4
|
+
require_relative "../grouping"
|
|
5
|
+
|
|
6
|
+
module E11y
|
|
7
|
+
module Devtools
|
|
8
|
+
module Tui
|
|
9
|
+
module Widgets
|
|
10
|
+
# Renders a scrollable list of interaction groups.
|
|
11
|
+
# Each row shows: bullet (● error / ○ ok), time, trace count.
|
|
12
|
+
class InteractionList
|
|
13
|
+
def initialize(interactions:, selected_index: 0, source_filter: :all)
|
|
14
|
+
@interactions = interactions
|
|
15
|
+
@selected_index = selected_index
|
|
16
|
+
@source_filter = source_filter
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render(tui, frame, area)
|
|
20
|
+
rows = @interactions.map do |ix|
|
|
21
|
+
bullet = ix.has_error? ? "●" : "○"
|
|
22
|
+
bullet_fg = ix.has_error? ? :red : :gray
|
|
23
|
+
time_str = ix.started_at.strftime("%H:%M:%S")
|
|
24
|
+
count_str = "#{ix.trace_ids.size} req"
|
|
25
|
+
error_str = ix.has_error? ? " ● err" : ""
|
|
26
|
+
|
|
27
|
+
tui.line(spans: [
|
|
28
|
+
tui.span(content: bullet, style: { fg: bullet_fg }),
|
|
29
|
+
tui.span(content: " #{time_str} #{count_str}#{error_str}")
|
|
30
|
+
])
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
frame.render_widget(
|
|
34
|
+
tui.list(
|
|
35
|
+
items: rows,
|
|
36
|
+
highlight_style: { bg: :dark_gray },
|
|
37
|
+
selected_index: @selected_index,
|
|
38
|
+
block: tui.block(title: " INTERACTIONS ", borders: :all)
|
|
39
|
+
),
|
|
40
|
+
area
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "e11y"
|
|
4
|
+
require_relative "devtools/version"
|
|
5
|
+
|
|
6
|
+
module E11y
|
|
7
|
+
# Developer tooling for E11y: TUI, Browser Overlay, and MCP Server.
|
|
8
|
+
module Devtools
|
|
9
|
+
autoload :Tui, "e11y/devtools/tui"
|
|
10
|
+
autoload :Overlay, "e11y/devtools/overlay"
|
|
11
|
+
autoload :Mcp, "e11y/devtools/mcp"
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "json"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "time"
|
|
8
|
+
require "fileutils"
|
|
9
|
+
require "e11y/adapters/dev_log/query"
|
|
10
|
+
|
|
11
|
+
# Load tool files (guarded in case mcp gem is absent)
|
|
12
|
+
%w[
|
|
13
|
+
recent_events events_by_trace search stats errors clear
|
|
14
|
+
].each do |name|
|
|
15
|
+
require "e11y/devtools/mcp/tools/#{name}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
RSpec.describe E11y::Devtools::Mcp::Tools do
|
|
19
|
+
let(:error_severities) { %w[error fatal] }
|
|
20
|
+
let(:dir) { Dir.mktmpdir("e11y_mcp") }
|
|
21
|
+
let(:path) { File.join(dir, "e11y_dev.jsonl") }
|
|
22
|
+
let(:store) { E11y::Adapters::DevLog::Query.new(path) }
|
|
23
|
+
let(:ctx) { { store: store } }
|
|
24
|
+
|
|
25
|
+
after { FileUtils.remove_entry(dir) }
|
|
26
|
+
|
|
27
|
+
def write_event(overrides = {})
|
|
28
|
+
data = {
|
|
29
|
+
"id" => SecureRandom.uuid,
|
|
30
|
+
"timestamp" => Time.now.iso8601(3),
|
|
31
|
+
"event_name" => "test.event",
|
|
32
|
+
"severity" => "info",
|
|
33
|
+
"trace_id" => "t1",
|
|
34
|
+
"payload" => {},
|
|
35
|
+
"metadata" => {}
|
|
36
|
+
}.merge(overrides)
|
|
37
|
+
File.open(path, "a") { |f| f.puts(JSON.generate(data)) }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
describe E11y::Devtools::Mcp::Tools::RecentEvents do
|
|
41
|
+
it "returns recent events as array" do
|
|
42
|
+
write_event
|
|
43
|
+
result = described_class.call(limit: 10, server_context: ctx)
|
|
44
|
+
expect(result).to be_an(Array)
|
|
45
|
+
expect(result.first["event_name"]).to eq("test.event")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "respects limit" do
|
|
49
|
+
5.times { write_event }
|
|
50
|
+
result = described_class.call(limit: 2, server_context: ctx)
|
|
51
|
+
expect(result.size).to eq(2)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "filters by severity" do
|
|
55
|
+
write_event("severity" => "info")
|
|
56
|
+
write_event("severity" => "error")
|
|
57
|
+
result = described_class.call(limit: 10, severity: "error", server_context: ctx)
|
|
58
|
+
expect(result.all? { |e| e["severity"] == "error" }).to be true
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe E11y::Devtools::Mcp::Tools::EventsByTrace do
|
|
63
|
+
it "returns events for given trace_id" do
|
|
64
|
+
write_event("trace_id" => "abc", "event_name" => "a")
|
|
65
|
+
write_event("trace_id" => "xyz", "event_name" => "b")
|
|
66
|
+
result = described_class.call(trace_id: "abc", server_context: ctx)
|
|
67
|
+
expect(result.size).to eq(1)
|
|
68
|
+
expect(result.first["event_name"]).to eq("a")
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe E11y::Devtools::Mcp::Tools::Search do
|
|
73
|
+
it "finds events matching query" do
|
|
74
|
+
write_event("event_name" => "payment.failed")
|
|
75
|
+
write_event("event_name" => "order.created")
|
|
76
|
+
result = described_class.call(query: "payment", limit: 10, server_context: ctx)
|
|
77
|
+
expect(result.size).to eq(1)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
describe E11y::Devtools::Mcp::Tools::Stats do
|
|
82
|
+
it "returns stats hash" do
|
|
83
|
+
write_event
|
|
84
|
+
result = described_class.call(server_context: ctx)
|
|
85
|
+
expect(result).to be_a(Hash)
|
|
86
|
+
expect(result).to have_key(:total_events)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe E11y::Devtools::Mcp::Tools::Errors do
|
|
91
|
+
it "returns only error/fatal events" do
|
|
92
|
+
write_event("severity" => "info")
|
|
93
|
+
write_event("severity" => "error")
|
|
94
|
+
result = described_class.call(limit: 10, server_context: ctx)
|
|
95
|
+
expect(result.all? { |e| error_severities.include?(e["severity"]) }).to be true
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
describe E11y::Devtools::Mcp::Tools::Clear do
|
|
100
|
+
it "clears the log and returns confirmation string" do
|
|
101
|
+
write_event
|
|
102
|
+
result = described_class.call(server_context: ctx)
|
|
103
|
+
expect(result).to include("cleared")
|
|
104
|
+
expect(store.stored_events).to be_empty
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "spec_helper"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "json"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
require "e11y/adapters/dev_log/query"
|
|
8
|
+
require "e11y/devtools/overlay/controller"
|
|
9
|
+
|
|
10
|
+
RSpec.describe E11y::Devtools::Overlay::Controller do
|
|
11
|
+
let(:dir) { Dir.mktmpdir("e11y_ctrl") }
|
|
12
|
+
let(:controller) { described_class.new(query) }
|
|
13
|
+
let(:path) { File.join(dir, "e11y_dev.jsonl") }
|
|
14
|
+
let(:query) { E11y::Adapters::DevLog::Query.new(path) }
|
|
15
|
+
|
|
16
|
+
after { FileUtils.remove_entry(dir) }
|
|
17
|
+
|
|
18
|
+
def write_event(name: "test.event", severity: "info", trace_id: "t1")
|
|
19
|
+
data = {
|
|
20
|
+
"id" => SecureRandom.uuid, "timestamp" => Time.now.iso8601(3),
|
|
21
|
+
"event_name" => name, "severity" => severity,
|
|
22
|
+
"trace_id" => trace_id, "payload" => {}, "metadata" => {}
|
|
23
|
+
}
|
|
24
|
+
File.open(path, "a") { |f| f.puts(JSON.generate(data)) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe "#events_for" do
|
|
28
|
+
it "returns events_by_trace when trace_id given" do
|
|
29
|
+
write_event(trace_id: "abc")
|
|
30
|
+
result = controller.events_for(trace_id: "abc")
|
|
31
|
+
expect(result).to be_an(Array)
|
|
32
|
+
expect(result.first["trace_id"]).to eq("abc")
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it "returns recent events when no trace_id" do
|
|
36
|
+
write_event
|
|
37
|
+
result = controller.events_for(trace_id: nil)
|
|
38
|
+
expect(result).to be_an(Array)
|
|
39
|
+
expect(result.size).to eq(1)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
describe "#recent_events" do
|
|
44
|
+
it "returns limited recent events" do
|
|
45
|
+
3.times { write_event }
|
|
46
|
+
result = controller.recent_events(limit: 2)
|
|
47
|
+
expect(result.size).to eq(2)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
describe "#clear_log!" do
|
|
52
|
+
it "removes the log file" do
|
|
53
|
+
write_event
|
|
54
|
+
controller.clear_log!
|
|
55
|
+
expect(File.exist?(path)).to be false
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|