main
py 72 lines 2.31 KB
Raw
1 """Structured, argument-free MCP audit events."""
2 from __future__ import annotations
3
4 import json
5 import logging
6 from datetime import datetime, timezone
7 from typing import Any, Protocol
8
9 from opentelemetry import trace
10
11 from app.contracts import McpAuthContext, McpRiskClass, McpToolKind, sanitize_response_data
12
13
14 class McpAuditSink(Protocol):
15 def emit(
16 self,
17 event: str,
18 *,
19 tool: str,
20 request_id: str,
21 auth: McpAuthContext,
22 risk_class: McpRiskClass | None = None,
23 kind: McpToolKind | None = None,
24 code: str | None = None,
25 duration_ms: int | None = None,
26 details: dict[str, Any] | None = None,
27 ) -> None: ...
28
29
30 class StructuredMcpAuditLogger:
31 def __init__(self, logger: logging.Logger | None = None, *, environment: str = "LOCAL") -> None:
32 self.logger = logger or logging.getLogger("aip.mcp.audit")
33 self.environment = environment
34
35 def emit(
36 self,
37 event: str,
38 *,
39 tool: str,
40 request_id: str,
41 auth: McpAuthContext,
42 risk_class: McpRiskClass | None = None,
43 kind: McpToolKind | None = None,
44 code: str | None = None,
45 duration_ms: int | None = None,
46 details: dict[str, Any] | None = None,
47 ) -> None:
48 record = {
49 "timestamp": datetime.now(timezone.utc).isoformat(),
50 "service": "mcp-gateway",
51 "environment": self.environment,
52 "level": "INFO",
53 "event": event,
54 "tool": tool,
55 "requestId": request_id,
56 "serviceIdentity": auth.service_identity,
57 "authenticationType": auth.authentication_type.value,
58 }
59 if risk_class is not None:
60 record["riskClass"] = risk_class.value
61 if kind is not None:
62 record["toolKind"] = kind.value
63 if code is not None:
64 record["code"] = code
65 if duration_ms is not None:
66 record["durationMs"] = duration_ms
67 if details:
68 record["details"] = sanitize_response_data(details)
69 span_context = trace.get_current_span().get_span_context()
70 if span_context.is_valid:
71 record["traceId"] = format(span_context.trace_id, "032x")
72 self.logger.info(json.dumps(record, separators=(",", ":"), sort_keys=True))