e11y-devtools 0.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 +7 -0
- data/README.md +137 -0
- data/exe/e11y +34 -0
- data/lib/e11y/devtools/mcp/server.rb +96 -0
- data/lib/e11y/devtools/mcp/tool_base.rb +25 -0
- data/lib/e11y/devtools/mcp/tools/clear.rb +31 -0
- data/lib/e11y/devtools/mcp/tools/errors.rb +35 -0
- data/lib/e11y/devtools/mcp/tools/event_detail.rb +33 -0
- data/lib/e11y/devtools/mcp/tools/events_by_trace.rb +33 -0
- data/lib/e11y/devtools/mcp/tools/interactions.rb +40 -0
- data/lib/e11y/devtools/mcp/tools/recent_events.rb +34 -0
- data/lib/e11y/devtools/mcp/tools/search.rb +34 -0
- data/lib/e11y/devtools/mcp/tools/stats.rb +30 -0
- data/lib/e11y/devtools/overlay/controller.rb +54 -0
- data/lib/e11y/devtools/overlay/engine.rb +26 -0
- data/lib/e11y/devtools/overlay/middleware.rb +80 -0
- data/lib/e11y/devtools/overlay/rails_controller.rb +42 -0
- data/lib/e11y/devtools/tui/app.rb +262 -0
- data/lib/e11y/devtools/tui/grouping.rb +66 -0
- data/lib/e11y/devtools/tui/widgets/event_detail.rb +62 -0
- data/lib/e11y/devtools/tui/widgets/event_list.rb +70 -0
- data/lib/e11y/devtools/tui/widgets/interaction_list.rb +47 -0
- data/lib/e11y/devtools/version.rb +8 -0
- data/lib/e11y/devtools.rb +13 -0
- metadata +116 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 102fbf2c428a06eba9c77929f68c2e4843cb67cb7b46d1f83540997cee1a16be
|
|
4
|
+
data.tar.gz: 7c990a1c47e5e4a631efcb6ecb658cc1898e9530243c88d017cc0d13454b6c00
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c1fcdccf770d5f98fcac0e04b57f1b708b910eff26bfeeb70dbe1da87465ca5e88afa384692a706f16ea711b3ef5be02abf45ea465218698ca192fb3adfbf639
|
|
7
|
+
data.tar.gz: b48620b09a3327808f33ee3e7d3609652501a9450cf32b23c57238aa65b82ee5e552ed41544560821beb75c010ca07b7096d9cb79ec3c1ee4b2183f0ed70268e
|
data/README.md
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# e11y-devtools
|
|
2
|
+
|
|
3
|
+
Developer tools for [e11y](https://github.com/aseletskiy/e11y) — the Rails observability gem.
|
|
4
|
+
|
|
5
|
+
Three complementary viewers for the same JSONL log:
|
|
6
|
+
|
|
7
|
+
| Viewer | How to use |
|
|
8
|
+
|--------|-----------|
|
|
9
|
+
| **TUI** (terminal) | `bundle exec e11y` |
|
|
10
|
+
| **Browser Overlay** | Automatic in development — floating badge in bottom-right |
|
|
11
|
+
| **MCP Server** | `bundle exec e11y mcp` — AI integration for Cursor / Claude Code |
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
Add to your Gemfile (development group only):
|
|
16
|
+
|
|
17
|
+
```ruby
|
|
18
|
+
# Gemfile
|
|
19
|
+
gem "e11y", "~> 1.0"
|
|
20
|
+
gem "e11y-devtools", "~> 0.1.0", group: :development
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Then run `bundle install`.
|
|
24
|
+
|
|
25
|
+
## TUI — Interactive Log Viewer
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
bundle exec e11y # Open TUI (default)
|
|
29
|
+
bundle exec e11y tui # Same as above
|
|
30
|
+
bundle exec e11y tail # Stream events to stdout
|
|
31
|
+
bundle exec e11y help # Show help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### TUI Keyboard Shortcuts
|
|
35
|
+
|
|
36
|
+
| Key | Action |
|
|
37
|
+
|-----|--------|
|
|
38
|
+
| `↑` / `k` | Move up |
|
|
39
|
+
| `↓` / `j` | Move down |
|
|
40
|
+
| `Enter` | Drill in (interactions → events → detail) |
|
|
41
|
+
| `Esc` / `b` | Go back |
|
|
42
|
+
| `w` | Filter: web requests only |
|
|
43
|
+
| `j` | Filter: background jobs only |
|
|
44
|
+
| `a` | Filter: all sources |
|
|
45
|
+
| `r` | Reload manually |
|
|
46
|
+
| `c` | Copy event JSON to clipboard (in detail view) |
|
|
47
|
+
| `q` | Quit |
|
|
48
|
+
|
|
49
|
+
## Browser Overlay
|
|
50
|
+
|
|
51
|
+
When `gem "e11y-devtools"` is in your Gemfile, the overlay badge appears automatically in development. No configuration needed — the Railtie mounts it.
|
|
52
|
+
|
|
53
|
+
The badge shows:
|
|
54
|
+
- Total event count for the current request
|
|
55
|
+
- Error count (red badge when errors present)
|
|
56
|
+
- Click to expand the slide-in panel with full event list
|
|
57
|
+
|
|
58
|
+
## MCP Server — AI Integration
|
|
59
|
+
|
|
60
|
+
Start the server:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
# stdio (for Cursor / Claude Code)
|
|
64
|
+
bundle exec e11y mcp
|
|
65
|
+
|
|
66
|
+
# HTTP (for direct integration)
|
|
67
|
+
bundle exec e11y mcp --port 3099
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Add to `.cursor/mcp.json` or `~/.claude/mcp.json`:
|
|
71
|
+
|
|
72
|
+
```json
|
|
73
|
+
{
|
|
74
|
+
"mcpServers": {
|
|
75
|
+
"e11y": {
|
|
76
|
+
"command": "bundle",
|
|
77
|
+
"args": ["exec", "e11y", "mcp"],
|
|
78
|
+
"cwd": "/path/to/your/rails/app"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Available MCP Tools
|
|
85
|
+
|
|
86
|
+
| Tool | Description |
|
|
87
|
+
|------|-------------|
|
|
88
|
+
| `recent_events` | Get latest N events (filterable by severity) |
|
|
89
|
+
| `events_by_trace` | Get all events for a trace ID |
|
|
90
|
+
| `search` | Full-text search across event names and payloads |
|
|
91
|
+
| `stats` | Aggregate statistics (total, by severity, oldest/newest) |
|
|
92
|
+
| `interactions` | Time-grouped interactions (parallel requests) |
|
|
93
|
+
| `event_detail` | Full payload for a single event by ID |
|
|
94
|
+
| `errors` | Recent error/fatal events only — fastest way to see what went wrong |
|
|
95
|
+
| `clear` | Clear the dev log |
|
|
96
|
+
|
|
97
|
+
## Configuration
|
|
98
|
+
|
|
99
|
+
```ruby
|
|
100
|
+
# config/initializers/e11y.rb
|
|
101
|
+
E11y.configure do |config|
|
|
102
|
+
config.register_adapter :dev_log, E11y::Adapters::DevLog.new(
|
|
103
|
+
path: Rails.root.join("log", "e11y_dev.jsonl"),
|
|
104
|
+
max_size: ENV.fetch("E11Y_MAX_SIZE", 50).to_i * 1024 * 1024, # 50 MB
|
|
105
|
+
max_lines: ENV.fetch("E11Y_MAX_EVENTS", 10_000).to_i,
|
|
106
|
+
keep_rotated: ENV.fetch("E11Y_KEEP_ROTATED", 5).to_i
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Environment Variables
|
|
112
|
+
|
|
113
|
+
| Variable | Default | Description |
|
|
114
|
+
|----------|---------|-------------|
|
|
115
|
+
| `E11Y_MAX_EVENTS` | `10000` | Max lines before rotation |
|
|
116
|
+
| `E11Y_MAX_SIZE` | `50` | Max log size in MB before rotation |
|
|
117
|
+
| `E11Y_KEEP_ROTATED` | `5` | Number of compressed `.gz` files to keep |
|
|
118
|
+
|
|
119
|
+
## Log Format
|
|
120
|
+
|
|
121
|
+
Events are stored as JSONL (one JSON object per line) at `log/e11y_dev.jsonl`.
|
|
122
|
+
Rotated files are numbered and gzip-compressed: `e11y_dev.jsonl.1.gz`, `.2.gz`, etc.
|
|
123
|
+
|
|
124
|
+
## Architecture
|
|
125
|
+
|
|
126
|
+
```
|
|
127
|
+
log/e11y_dev.jsonl ← E11y::Adapters::DevLog (write)
|
|
128
|
+
↓
|
|
129
|
+
E11y::Adapters::DevLog::Query (read, cache, search, grouping)
|
|
130
|
+
↓
|
|
131
|
+
┌─────┴──────┬──────────────┬──────────────┐
|
|
132
|
+
TUI Browser MCP Server
|
|
133
|
+
(ratatui_ruby) Overlay (gem 'mcp')
|
|
134
|
+
(Rack)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The JSONL file is the single source of truth. All viewers are stateless readers — they never write.
|
data/exe/e11y
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "e11y/devtools"
|
|
5
|
+
|
|
6
|
+
command = ARGV.shift || "tui"
|
|
7
|
+
|
|
8
|
+
case command
|
|
9
|
+
when "tui"
|
|
10
|
+
require "e11y/devtools/tui/app"
|
|
11
|
+
E11y::Devtools::Tui::App.new.run
|
|
12
|
+
when "mcp"
|
|
13
|
+
require "e11y/devtools/mcp/server"
|
|
14
|
+
E11y::Devtools::Mcp::Server.new.run(
|
|
15
|
+
transport: ARGV.include?("--port") ? :http : :stdio,
|
|
16
|
+
port: (ARGV[ARGV.index("--port") + 1] if ARGV.include?("--port"))&.to_i
|
|
17
|
+
)
|
|
18
|
+
when "tail"
|
|
19
|
+
require "e11y/devtools/tui/tail"
|
|
20
|
+
E11y::Devtools::Tui::Tail.new.run
|
|
21
|
+
when "help", "--help", "-h"
|
|
22
|
+
puts <<~HELP
|
|
23
|
+
bundle exec e11y [command]
|
|
24
|
+
|
|
25
|
+
Commands:
|
|
26
|
+
tui (default) Interactive TUI — browse events and traces
|
|
27
|
+
mcp MCP server for Cursor / Claude Code AI integration
|
|
28
|
+
tail Stream new events to stdout (pipe-friendly)
|
|
29
|
+
help Show this help
|
|
30
|
+
HELP
|
|
31
|
+
else
|
|
32
|
+
warn "Unknown command: #{command}. Run `bundle exec e11y help`."
|
|
33
|
+
exit 1
|
|
34
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
require "e11y/adapters/dev_log/query"
|
|
5
|
+
require_relative "tools/recent_events"
|
|
6
|
+
require_relative "tools/events_by_trace"
|
|
7
|
+
require_relative "tools/search"
|
|
8
|
+
require_relative "tools/stats"
|
|
9
|
+
require_relative "tools/interactions"
|
|
10
|
+
require_relative "tools/event_detail"
|
|
11
|
+
require_relative "tools/errors"
|
|
12
|
+
require_relative "tools/clear"
|
|
13
|
+
|
|
14
|
+
module E11y
|
|
15
|
+
module Devtools
|
|
16
|
+
module Mcp
|
|
17
|
+
# MCP Server wrapping the E11y DevLog adapter.
|
|
18
|
+
#
|
|
19
|
+
# Exposes 8 tools to AI tools like Cursor and Claude Code.
|
|
20
|
+
# Supports stdio (default) and StreamableHTTP transports.
|
|
21
|
+
#
|
|
22
|
+
# @example Start stdio server
|
|
23
|
+
# E11y::Devtools::Mcp::Server.new.run
|
|
24
|
+
#
|
|
25
|
+
# @example Start HTTP server on port 3099
|
|
26
|
+
# E11y::Devtools::Mcp::Server.new.run(transport: :http, port: 3099)
|
|
27
|
+
class Server
|
|
28
|
+
TOOLS = [
|
|
29
|
+
Tools::RecentEvents, Tools::EventsByTrace, Tools::Search,
|
|
30
|
+
Tools::Stats, Tools::Interactions, Tools::EventDetail,
|
|
31
|
+
Tools::Errors, Tools::Clear
|
|
32
|
+
].freeze
|
|
33
|
+
|
|
34
|
+
def initialize(log_path: nil)
|
|
35
|
+
@log_path = log_path || auto_detect_log_path
|
|
36
|
+
@store = E11y::Adapters::DevLog::Query.new(@log_path)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Start the MCP server.
|
|
40
|
+
#
|
|
41
|
+
# @param transport [:stdio, :http] Transport to use
|
|
42
|
+
# @param port [Integer, nil] HTTP port (default 3099)
|
|
43
|
+
def run(transport: :stdio, port: nil)
|
|
44
|
+
require "mcp"
|
|
45
|
+
server = build_mcp_server
|
|
46
|
+
case transport
|
|
47
|
+
when :stdio then run_stdio(server)
|
|
48
|
+
when :http then run_http(server, port || 3099)
|
|
49
|
+
else raise ArgumentError, "Unknown transport: #{transport}"
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def build_mcp_server
|
|
56
|
+
MCP::Server.new(
|
|
57
|
+
name: "e11y",
|
|
58
|
+
version: E11y::Devtools::VERSION,
|
|
59
|
+
tools: TOOLS,
|
|
60
|
+
server_context: { store: @store }
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def run_stdio(server)
|
|
65
|
+
t = MCP::Server::Transports::StdioTransport.new(server)
|
|
66
|
+
server.transport = t
|
|
67
|
+
t.open
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def run_http(server, port)
|
|
71
|
+
require "webrick"
|
|
72
|
+
t = MCP::Server::Transports::StreamableHTTPTransport.new(server)
|
|
73
|
+
server.transport = t
|
|
74
|
+
s = WEBrick::HTTPServer.new(Port: port, Logger: WEBrick::Log.new(nil))
|
|
75
|
+
s.mount("/mcp", t)
|
|
76
|
+
trap("INT") { s.shutdown }
|
|
77
|
+
s.start
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def auto_detect_log_path
|
|
81
|
+
dir = Pathname.new(Dir.pwd)
|
|
82
|
+
loop do
|
|
83
|
+
candidate = dir.join("log", "e11y_dev.jsonl")
|
|
84
|
+
return candidate.to_s if candidate.exist?
|
|
85
|
+
|
|
86
|
+
parent = dir.parent
|
|
87
|
+
break if parent == dir
|
|
88
|
+
|
|
89
|
+
dir = parent
|
|
90
|
+
end
|
|
91
|
+
"log/e11y_dev.jsonl"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module E11y
|
|
4
|
+
module Devtools
|
|
5
|
+
module Mcp
|
|
6
|
+
# Conditional base: use MCP::Tool if available, otherwise plain class.
|
|
7
|
+
# This allows tests to run without the mcp gem installed.
|
|
8
|
+
ToolBase = if defined?(MCP::Tool)
|
|
9
|
+
MCP::Tool
|
|
10
|
+
else
|
|
11
|
+
Class.new do
|
|
12
|
+
def self.description(desc = nil)
|
|
13
|
+
@description = desc if desc
|
|
14
|
+
@description
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.input_schema(schema = nil)
|
|
18
|
+
@input_schema = schema if schema
|
|
19
|
+
@input_schema
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "mcp"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# mcp gem not available — ToolBase will use plain class
|
|
7
|
+
end
|
|
8
|
+
require_relative "../tool_base"
|
|
9
|
+
|
|
10
|
+
module E11y
|
|
11
|
+
module Devtools
|
|
12
|
+
module Mcp
|
|
13
|
+
module Tools
|
|
14
|
+
# Clears the E11y development log file.
|
|
15
|
+
class Clear < ToolBase
|
|
16
|
+
description "Clear the E11y development log file"
|
|
17
|
+
|
|
18
|
+
input_schema(
|
|
19
|
+
type: :object,
|
|
20
|
+
properties: {}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def self.call(server_context:, **_opts)
|
|
24
|
+
server_context[:store].clear!
|
|
25
|
+
"E11y log cleared successfully"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "mcp"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# mcp gem not available — ToolBase will use plain class
|
|
7
|
+
end
|
|
8
|
+
require_relative "../tool_base"
|
|
9
|
+
|
|
10
|
+
module E11y
|
|
11
|
+
module Devtools
|
|
12
|
+
module Mcp
|
|
13
|
+
module Tools
|
|
14
|
+
# Returns recent error and fatal events only.
|
|
15
|
+
class Errors < ToolBase
|
|
16
|
+
ERROR_SEVERITIES = %w[error fatal].freeze
|
|
17
|
+
|
|
18
|
+
description "Get recent error and fatal events only"
|
|
19
|
+
|
|
20
|
+
input_schema(
|
|
21
|
+
type: :object,
|
|
22
|
+
properties: {
|
|
23
|
+
limit: { type: :integer, description: "Max events", default: 20 }
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def self.call(server_context:, limit: 20)
|
|
28
|
+
events = server_context[:store].stored_events(limit: limit * 5)
|
|
29
|
+
events.select { |e| ERROR_SEVERITIES.include?(e["severity"]) }.first(limit)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "mcp"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# mcp gem not available — ToolBase will use plain class
|
|
7
|
+
end
|
|
8
|
+
require_relative "../tool_base"
|
|
9
|
+
|
|
10
|
+
module E11y
|
|
11
|
+
module Devtools
|
|
12
|
+
module Mcp
|
|
13
|
+
module Tools
|
|
14
|
+
# Returns the full payload of a single event by ID.
|
|
15
|
+
class EventDetail < ToolBase
|
|
16
|
+
description "Get full payload of a single event by ID"
|
|
17
|
+
|
|
18
|
+
input_schema(
|
|
19
|
+
type: :object,
|
|
20
|
+
required: ["event_id"],
|
|
21
|
+
properties: {
|
|
22
|
+
event_id: { type: :string, description: "Event UUID" }
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def self.call(event_id:, server_context:)
|
|
27
|
+
server_context[:store].find_event(event_id) || { error: "Event #{event_id} not found" }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "mcp"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# mcp gem not available — ToolBase will use plain class
|
|
7
|
+
end
|
|
8
|
+
require_relative "../tool_base"
|
|
9
|
+
|
|
10
|
+
module E11y
|
|
11
|
+
module Devtools
|
|
12
|
+
module Mcp
|
|
13
|
+
module Tools
|
|
14
|
+
# Returns all events for a specific trace ID in chronological order.
|
|
15
|
+
class EventsByTrace < ToolBase
|
|
16
|
+
description "Get all events for a specific trace ID in chronological order"
|
|
17
|
+
|
|
18
|
+
input_schema(
|
|
19
|
+
type: :object,
|
|
20
|
+
required: ["trace_id"],
|
|
21
|
+
properties: {
|
|
22
|
+
trace_id: { type: :string, description: "Trace ID" }
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def self.call(trace_id:, server_context:)
|
|
27
|
+
server_context[:store].events_by_trace(trace_id)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "mcp"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# mcp gem not available — ToolBase will use plain class
|
|
7
|
+
end
|
|
8
|
+
require_relative "../tool_base"
|
|
9
|
+
|
|
10
|
+
module E11y
|
|
11
|
+
module Devtools
|
|
12
|
+
module Mcp
|
|
13
|
+
module Tools
|
|
14
|
+
# Returns time-grouped interactions (parallel requests from one user action).
|
|
15
|
+
class Interactions < ToolBase
|
|
16
|
+
description "Get time-grouped interactions (parallel requests from one user action)"
|
|
17
|
+
|
|
18
|
+
input_schema(
|
|
19
|
+
type: :object,
|
|
20
|
+
properties: {
|
|
21
|
+
limit: { type: :integer, description: "Max interactions", default: 20 },
|
|
22
|
+
window_ms: { type: :integer, description: "Grouping window in ms", default: 500 }
|
|
23
|
+
}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
def self.call(server_context:, limit: 20, window_ms: 500)
|
|
27
|
+
server_context[:store].interactions(limit: limit, window_ms: window_ms).map do |ix|
|
|
28
|
+
{
|
|
29
|
+
started_at: ix.started_at.iso8601(3),
|
|
30
|
+
trace_ids: ix.trace_ids,
|
|
31
|
+
has_error: ix.has_error?,
|
|
32
|
+
traces_count: ix.traces_count
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "mcp"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# mcp gem not available — ToolBase will use plain class
|
|
7
|
+
end
|
|
8
|
+
require_relative "../tool_base"
|
|
9
|
+
|
|
10
|
+
module E11y
|
|
11
|
+
module Devtools
|
|
12
|
+
module Mcp
|
|
13
|
+
module Tools
|
|
14
|
+
# Returns the most recent events from the dev log.
|
|
15
|
+
class RecentEvents < ToolBase
|
|
16
|
+
description "Get recent E11y events from the development log"
|
|
17
|
+
|
|
18
|
+
input_schema(
|
|
19
|
+
type: :object,
|
|
20
|
+
properties: {
|
|
21
|
+
limit: { type: :integer, description: "Max events to return (default 50)", default: 50 },
|
|
22
|
+
severity: { type: :string, description: "Filter by severity",
|
|
23
|
+
enum: %w[debug info warn error fatal] }
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def self.call(server_context:, limit: 50, severity: nil)
|
|
28
|
+
server_context[:store].stored_events(limit: limit, severity: severity)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "mcp"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# mcp gem not available — ToolBase will use plain class
|
|
7
|
+
end
|
|
8
|
+
require_relative "../tool_base"
|
|
9
|
+
|
|
10
|
+
module E11y
|
|
11
|
+
module Devtools
|
|
12
|
+
module Mcp
|
|
13
|
+
module Tools
|
|
14
|
+
# Full-text search across event names and payload content.
|
|
15
|
+
class Search < ToolBase
|
|
16
|
+
description "Full-text search across event names and payload content"
|
|
17
|
+
|
|
18
|
+
input_schema(
|
|
19
|
+
type: :object,
|
|
20
|
+
required: ["query"],
|
|
21
|
+
properties: {
|
|
22
|
+
query: { type: :string, description: "Search term" },
|
|
23
|
+
limit: { type: :integer, description: "Max results", default: 50 }
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
def self.call(query:, server_context:, limit: 50)
|
|
28
|
+
server_context[:store].search(query, limit: limit)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
begin
|
|
4
|
+
require "mcp"
|
|
5
|
+
rescue LoadError
|
|
6
|
+
# mcp gem not available — ToolBase will use plain class
|
|
7
|
+
end
|
|
8
|
+
require_relative "../tool_base"
|
|
9
|
+
|
|
10
|
+
module E11y
|
|
11
|
+
module Devtools
|
|
12
|
+
module Mcp
|
|
13
|
+
module Tools
|
|
14
|
+
# Returns aggregate statistics about the E11y development log.
|
|
15
|
+
class Stats < ToolBase
|
|
16
|
+
description "Get aggregate statistics about the E11y development log"
|
|
17
|
+
|
|
18
|
+
input_schema(
|
|
19
|
+
type: :object,
|
|
20
|
+
properties: {}
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
def self.call(server_context:, **_opts)
|
|
24
|
+
server_context[:store].stats
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "e11y/adapters/dev_log/query"
|
|
4
|
+
require "e11y/adapters/dev_log"
|
|
5
|
+
|
|
6
|
+
module E11y
|
|
7
|
+
module Devtools
|
|
8
|
+
module Overlay
|
|
9
|
+
# Plain Ruby controller logic — testable without Rails.
|
|
10
|
+
# Used by the Rails route handlers (see config/routes.rb).
|
|
11
|
+
class Controller
|
|
12
|
+
def initialize(query = nil)
|
|
13
|
+
@query = query || resolve_query
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def events_for(trace_id: nil, limit: 50)
|
|
17
|
+
if trace_id && !trace_id.empty?
|
|
18
|
+
@query.events_by_trace(trace_id)
|
|
19
|
+
else
|
|
20
|
+
@query.stored_events(limit: limit)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def recent_events(limit: 50)
|
|
25
|
+
clamped = limit.to_i.clamp(1, 500)
|
|
26
|
+
@query.stored_events(limit: clamped)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def clear_log!
|
|
30
|
+
@query.clear!
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def stats
|
|
34
|
+
@query.stats
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def resolve_query
|
|
40
|
+
if defined?(E11y) && E11y.respond_to?(:configuration)
|
|
41
|
+
adapter = E11y.configuration.adapters[:dev_log]
|
|
42
|
+
return adapter if adapter.respond_to?(:stored_events)
|
|
43
|
+
end
|
|
44
|
+
default_path = if defined?(Rails) && Rails.respond_to?(:root)
|
|
45
|
+
Rails.root.join("log", "e11y_dev.jsonl").to_s
|
|
46
|
+
else
|
|
47
|
+
"log/e11y_dev.jsonl"
|
|
48
|
+
end
|
|
49
|
+
E11y::Adapters::DevLog::Query.new(default_path)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails"
|
|
4
|
+
|
|
5
|
+
module E11y
|
|
6
|
+
module Devtools
|
|
7
|
+
module Overlay
|
|
8
|
+
# Rails Engine that mounts JSON endpoints at /_e11y/
|
|
9
|
+
# and injects the overlay badge via Rack middleware.
|
|
10
|
+
class Engine < Rails::Engine
|
|
11
|
+
isolate_namespace E11y::Devtools::Overlay
|
|
12
|
+
|
|
13
|
+
initializer "e11y_devtools.overlay.middleware" do |app|
|
|
14
|
+
next unless Rails.env.development? || Rails.env.test?
|
|
15
|
+
|
|
16
|
+
require "e11y/devtools/overlay/middleware"
|
|
17
|
+
app.middleware.use E11y::Devtools::Overlay::Middleware
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
config.generators do |g|
|
|
21
|
+
g.test_framework :rspec
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|