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``.
24- Define the five metric objects above against a supplied or default
25 ``CollectorRegistry``.
26- Record one observation per tool call, rate-limit hit, redaction and
28- Start the Prometheus HTTP exporter on demand, in a daemon thread.
29- Degrade to a no-op stub when ``prometheus_client`` is not installed,
30 and never raise for the caller.
34Install the optional extra::
36 pip install 'oct[metrics]'
38Then start the server with ``--metrics-port``::
40 python -m oct.mcp --project-root /path/to/project --metrics-port 9090
46 L1 - errors: metrics server startup failures
47 L2 - lifecycle: metrics server started
48 L3 - (none): this module emits startup failures and lifecycle only
49 L4 - (none): no deep trace is emitted
53- This module never raises for the caller - all methods swallow errors
54 and degrade gracefully.
55- When ``prometheus_client`` is absent, ``PROMETHEUS_AVAILABLE`` is
56 ``False`` and :class:`McpMetrics` is a lightweight no-op stub.
57- The metrics HTTP server is started in a **daemon** background thread
58 so it does not prevent process exit.
59- :class:`McpMetrics` accepts an optional ``registry`` parameter
60 (``CollectorRegistry``). The default is the module-level
61 ``prometheus_client.REGISTRY`` so production ``/metrics`` works
62 without any caller change. Tests and multi-tenant callers pass a
63 private ``CollectorRegistry()`` to avoid ``Duplicated timeseries``
64 collisions across instances.
68- oct.core.diagnostics (_dbg structured logger)
69- prometheus_client (Counter, Histogram, CollectorRegistry and the /metrics HTTP exporter, optional)
72from __future__
import annotations
77 from prometheus_client
import (
81 start_http_server
as _start_http,
83 from prometheus_client.registry
import CollectorRegistry
85 PROMETHEUS_AVAILABLE: bool =
True
87 PROMETHEUS_AVAILABLE =
False
89 CollectorRegistry =
None
91from oct.core.diagnostics
import _dbg
95 """Prometheus instrumentation for the oct-mcp pipeline.
97 All record methods are no-ops when ``prometheus_client`` is absent.
98 Instantiate once per server session and pass into :class:`ServerState`.
101 def __init__(self, registry:
"CollectorRegistry | None" =
None) ->
None:
102 """Build the metric vectors.
107 Prometheus :class:`CollectorRegistry` to register metrics on.
108 ``None`` (default) uses the module-level global ``REGISTRY``,
109 which is what ``prometheus_client.start_http_server()`` exposes
110 on ``/metrics`` by default. Pass a private
111 ``CollectorRegistry()`` for tests or multi-tenant scenarios
112 that need isolation from other ``McpMetrics`` instances.
114 if not PROMETHEUS_AVAILABLE:
120 self.
_registry = registry
if registry
is not None else REGISTRY
123 "oct_mcp_tool_calls_total",
124 "Total MCP tool calls processed by oct-mcp",
125 [
"tool",
"decision"],
129 "oct_mcp_tool_duration_seconds",
130 "Wall-clock time from tool call receipt to response (seconds)",
135 "oct_mcp_rate_limit_hits_total",
136 "Tool calls rejected by the rate limiter",
141 "oct_mcp_redactions_total",
142 "Redaction substitutions applied to tool output",
147 "oct_mcp_validation_failures_total",
148 "Tool calls rejected by input validation",
158 """Start the Prometheus ``/metrics`` HTTP server on *port*.
160 Runs in a **daemon** background thread so it does not block the
161 MCP event loop. Silently no-ops when ``prometheus_client`` is
167 TCP port to bind. Must be in the range 1–65535.
169 if not PROMETHEUS_AVAILABLE:
171 func =
"McpMetrics.start_server"
176 t = threading.Thread(
183 _dbg(
"MCP", func, f
"Prometheus metrics server started on port {port}", 2)
184 except Exception
as exc:
185 _dbg(
"MCP", func, f
"SYSTEM_ERROR: failed to start metrics server: {exc}", 1)
194 """Record a completed (or denied) tool call.
199 MCP tool name, e.g. ``"oct_lint"``.
201 Pipeline decision: ``"allowed"``, ``"denied"``, or
202 ``"requires_confirmation"``.
204 Wall-clock duration in seconds.
206 Number of redaction substitutions applied to the output.
208 if not PROMETHEUS_AVAILABLE:
211 self.
_tool_calls.labels(tool=tool, decision=decision).inc()
219 """Increment the rate-limit rejection counter for *tool*."""
220 if not PROMETHEUS_AVAILABLE:
228 """Increment the validation-failure counter for *tool*."""
229 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)