Class: McpLogs::Call
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- McpLogs::Call
- Defined in:
- app/models/mcp_logs/call.rb
Overview
One JSON-RPC request served by an MCP server. Inserted as "running" the moment the request starts, updated when it finishes — so a long tool call is visible while it runs rather than only after it returns.
Constant Summary collapse
- STATUSES =
%w[running ok error].freeze
- ABANDONED =
error_type for a row whose finish never landed. Its own value rather than an exception class name, because no exception was raised here — the process that would have raised one is gone.
"abandoned".freeze
- ABANDONED_MESSAGE =
"No completion was ever recorded for this call. Whatever was " \ "serving it stopped first — a deploy, a crash, or a failed log " \ "write — so its result and duration are unknown.".freeze
- FILTER_OPTIONS_LIMIT =
How many distinct values a filter dropdown will offer. Bounded because tool_name is raw client input (see Recorder::STRING_LIMIT): the number of distinct values is capped by requests served, not by tools that exist, so an unbounded SELECT DISTINCT would let one looping client put an
200
Class Method Summary collapse
- .filter_values(column) ⇒ Object
- .purge!(retention_days = McpLogs.retention_days) ⇒ Object
-
.sweep_abandoned!(hours = McpLogs.abandoned_after_hours) ⇒ Object
A row is inserted "running" and updated when the request finishes.
- .tool_counts ⇒ Object
Instance Method Summary collapse
Class Method Details
.filter_values(column) ⇒ Object
37 38 39 |
# File 'app/models/mcp_logs/call.rb', line 37 def self.filter_values(column) where.not(column => nil).distinct.order(column).limit(FILTER_OPTIONS_LIMIT).pluck(column) end |
.purge!(retention_days = McpLogs.retention_days) ⇒ Object
45 46 47 |
# File 'app/models/mcp_logs/call.rb', line 45 def self.purge!(retention_days = McpLogs.retention_days) where(started_at: ...retention_days.to_i.days.ago).delete_all end |
.sweep_abandoned!(hours = McpLogs.abandoned_after_hours) ⇒ Object
A row is inserted "running" and updated when the request finishes. If that update never lands — the container restarted mid-call, or the write itself failed — nothing retries it, so the row would claim to be running for the entire retention window: a permanent false positive on the page an operator reads to answer "what is the agent doing right now", and an inflated count in tool_counts.
completed_at and duration_ms stay nil on purpose. The sweep knows the call did not finish; it does not know when it stopped, and stamping "now" would invent a fact. Returns how many rows it settled.
59 60 61 62 63 64 |
# File 'app/models/mcp_logs/call.rb', line 59 def self.sweep_abandoned!(hours = McpLogs.abandoned_after_hours) where(status: "running") .where(started_at: ...hours.to_i.hours.ago) .update_all(status: "error", error_type: ABANDONED, error_message: ABANDONED_MESSAGE, updated_at: Time.current) end |
.tool_counts ⇒ Object
41 42 43 |
# File 'app/models/mcp_logs/call.rb', line 41 def self.tool_counts where.not(tool_name: nil).group(:tool_name).count end |
Instance Method Details
#duration_seconds ⇒ Object
68 |
# File 'app/models/mcp_logs/call.rb', line 68 def duration_seconds = duration_ms && duration_ms / 1000.0 |
#running? ⇒ Boolean
66 |
# File 'app/models/mcp_logs/call.rb', line 66 def running? = status == "running" |