8Optional Prometheus instrumentation for the ``oct-mcp`` server (Phase 5C).
12* ``oct_mcp_tool_calls_total`` — Counter; labels ``tool``, ``decision``.
13* ``oct_mcp_tool_duration_seconds`` — Histogram; label ``tool``.
14* ``oct_mcp_rate_limit_hits_total`` — Counter; label ``tool``.
15* ``oct_mcp_redactions_total`` — Counter; label ``tool``.
16* ``oct_mcp_validation_failures_total`` — Counter; label ``tool``.
18All public methods are **no-ops** when ``prometheus_client`` is not
19installed, so the rest of the pipeline never needs to guard with
20``if metrics is not None``.
24Install the optional extra::
26 pip install 'oct[metrics]'
28Then start the server with ``--metrics-port``::
30 python -m oct.mcp --project-root /path/to/project --metrics-port 9090
36 L1 — errors: metrics server startup failures
37 L2 — lifecycle: metrics server started
41- This module never raises for the caller — all methods swallow errors
42 and degrade gracefully.
43- When ``prometheus_client`` is absent, ``PROMETHEUS_AVAILABLE`` is
44 ``False`` and :class:`McpMetrics` is a lightweight no-op stub.
45- The metrics HTTP server is started in a **daemon** background thread
46 so it does not prevent process exit.
47- :class:`McpMetrics` accepts an optional ``registry`` parameter
48 (``CollectorRegistry``). The default is the module-level
49 ``prometheus_client.REGISTRY`` so production ``/metrics`` works
50 without any caller change. Tests and multi-tenant callers pass a
51 private ``CollectorRegistry()`` to avoid ``Duplicated timeseries``
52 collisions across instances.
55from __future__
import annotations
60 from prometheus_client
import (
64 start_http_server
as _start_http,
66 from prometheus_client.registry
import CollectorRegistry
68 PROMETHEUS_AVAILABLE: bool =
True
70 PROMETHEUS_AVAILABLE =
False
72 CollectorRegistry =
None
75 from oc_diagnostics
import _dbg
as _real_dbg
77 def _dbg(*args, **kwargs) -> None:
78 _real_dbg(*args, **kwargs)
80 def _dbg(*args, **kwargs) -> None:
85 """Prometheus instrumentation for the oct-mcp pipeline.
87 All record methods are no-ops when ``prometheus_client`` is absent.
88 Instantiate once per server session and pass into :class:`ServerState`.
91 def __init__(self, registry:
"CollectorRegistry | None" =
None) ->
None:
92 """Build the metric vectors.
97 Prometheus :class:`CollectorRegistry` to register metrics on.
98 ``None`` (default) uses the module-level global ``REGISTRY``,
99 which is what ``prometheus_client.start_http_server()`` exposes
100 on ``/metrics`` by default. Pass a private
101 ``CollectorRegistry()`` for tests or multi-tenant scenarios
102 that need isolation from other ``McpMetrics`` instances.
104 if not PROMETHEUS_AVAILABLE:
110 self.
_registry = registry
if registry
is not None else REGISTRY
113 "oct_mcp_tool_calls_total",
114 "Total MCP tool calls processed by oct-mcp",
115 [
"tool",
"decision"],
119 "oct_mcp_tool_duration_seconds",
120 "Wall-clock time from tool call receipt to response (seconds)",
125 "oct_mcp_rate_limit_hits_total",
126 "Tool calls rejected by the rate limiter",
131 "oct_mcp_redactions_total",
132 "Redaction substitutions applied to tool output",
137 "oct_mcp_validation_failures_total",
138 "Tool calls rejected by input validation",
148 """Start the Prometheus ``/metrics`` HTTP server on *port*.
150 Runs in a **daemon** background thread so it does not block the
151 MCP event loop. Silently no-ops when ``prometheus_client`` is
157 TCP port to bind. Must be in the range 1–65535.
159 if not PROMETHEUS_AVAILABLE:
161 func =
"McpMetrics.start_server"
166 t = threading.Thread(
173 _dbg(
"MCP", func, f
"Prometheus metrics server started on port {port}", 2)
174 except Exception
as exc:
175 _dbg(
"MCP", func, f
"SYSTEM_ERROR: failed to start metrics server: {exc}", 1)
184 """Record a completed (or denied) tool call.
189 MCP tool name, e.g. ``"oct_lint"``.
191 Pipeline decision: ``"allowed"``, ``"denied"``, or
192 ``"requires_confirmation"``.
194 Wall-clock duration in seconds.
196 Number of redaction substitutions applied to the output.
198 if not PROMETHEUS_AVAILABLE:
201 self.
_tool_calls.labels(tool=tool, decision=decision).inc()
209 """Increment the rate-limit rejection counter for *tool*."""
210 if not PROMETHEUS_AVAILABLE:
218 """Increment the validation-failure counter for *tool*."""
219 if not PROMETHEUS_AVAILABLE:
None __init__(self, "CollectorRegistry | None" registry=None)
None start_server(self, int port)
Counter _validation_failures
None record_validation_failure(self, str tool)
None record_rate_limit_hit(self, str tool)
None record_call(self, str tool, str decision, float duration_s, int redactions)
None _dbg(*args, **kwargs)