Skip to content

Commit 108bf55

Browse files
committed
feat: 完成阶段七监控审计层(monitor)
1 parent 4a48493 commit 108bf55

1 file changed

Lines changed: 235 additions & 0 deletions

File tree

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
2+
#
3+
# Copyright (C) 2026 Tencent. All rights reserved.
4+
#
5+
# tRPC-Agent-Python is licensed under Apache-2.0.
6+
"""Monitoring and audit layer for the code review agent.
7+
8+
Records per-review telemetry including:
9+
- Total duration and sandbox execution duration
10+
- Tool call and filter intercept counts
11+
- Finding count and severity distribution
12+
- Exception type distribution
13+
14+
All metrics are persisted to the monitor_summary table in the database.
15+
"""
16+
17+
from __future__ import annotations
18+
19+
import json
20+
import time
21+
import traceback
22+
from collections import Counter
23+
from dataclasses import dataclass, field
24+
from typing import Optional
25+
26+
from .db.storage import StorageABC
27+
from .models import Finding, MonitorSummary, ReviewTask
28+
29+
30+
# ──────────────────────────────────────────────
31+
# Data structures
32+
# ──────────────────────────────────────────────
33+
34+
35+
@dataclass
36+
class ReviewMetrics:
37+
"""Collected metrics for a single review session."""
38+
39+
# Timing
40+
total_duration_ms: float = 0.0
41+
sandbox_duration_ms: float = 0.0
42+
parse_duration_ms: float = 0.0
43+
filter_duration_ms: float = 0.0
44+
45+
# Counters
46+
tool_call_count: int = 0
47+
intercept_count: int = 0
48+
finding_count: int = 0
49+
50+
# Distributions
51+
severity_counts: dict[str, int] = field(default_factory=dict)
52+
exception_types: list[str] = field(default_factory=list)
53+
54+
# Errors
55+
errors: list[str] = field(default_factory=list)
56+
57+
def to_monitor_summary(self, task_id: str) -> MonitorSummary:
58+
"""Convert metrics to a MonitorSummary model for DB persistence."""
59+
return MonitorSummary(
60+
task_id=task_id,
61+
total_duration_ms=self.total_duration_ms,
62+
sandbox_duration_ms=self.sandbox_duration_ms,
63+
tool_call_count=self.tool_call_count,
64+
intercept_count=self.intercept_count,
65+
finding_count=self.finding_count,
66+
severity_distribution=json.dumps(self.severity_counts, ensure_ascii=False),
67+
exception_types=json.dumps(self.exception_types, ensure_ascii=False),
68+
)
69+
70+
71+
# ──────────────────────────────────────────────
72+
# 7.1 + 7.2: Monitor
73+
# ──────────────────────────────────────────────
74+
75+
76+
class ReviewMonitor:
77+
"""Collects and persists review telemetry.
78+
79+
Usage:
80+
monitor = ReviewMonitor(storage, task_id)
81+
monitor.start()
82+
# ... run review ...
83+
monitor.record_sandbox(duration_ms=1500)
84+
monitor.record_tool_call()
85+
monitor.record_findings(findings)
86+
monitor.record_exception(e)
87+
monitor.finish() # saves to DB
88+
"""
89+
90+
def __init__(self, storage: StorageABC, task_id: str):
91+
self.storage = storage
92+
self.task_id = task_id
93+
self.metrics = ReviewMetrics()
94+
self._start_time: Optional[float] = None
95+
96+
# ── Timing ──
97+
98+
def start(self) -> None:
99+
"""Start the review timer."""
100+
self._start_time = time.monotonic()
101+
102+
def finish(self) -> MonitorSummary:
103+
"""Stop the timer and persist metrics to the database.
104+
105+
Returns:
106+
The saved MonitorSummary.
107+
"""
108+
if self._start_time is not None:
109+
self.metrics.total_duration_ms = (time.monotonic() - self._start_time) * 1000
110+
111+
summary = self.metrics.to_monitor_summary(self.task_id)
112+
self.storage.save_monitor_summary(summary)
113+
return summary
114+
115+
# ── Individual recorders ──
116+
117+
def record_sandbox_duration(self, duration_ms: float) -> None:
118+
"""Record sandbox execution duration."""
119+
self.metrics.sandbox_duration_ms += duration_ms
120+
121+
def record_parse_duration(self, duration_ms: float) -> None:
122+
"""Record diff parsing duration."""
123+
self.metrics.parse_duration_ms += duration_ms
124+
125+
def record_filter_duration(self, duration_ms: float) -> None:
126+
"""Record filter evaluation duration."""
127+
self.metrics.filter_duration_ms += duration_ms
128+
129+
def record_tool_call(self) -> None:
130+
"""Increment the tool call counter."""
131+
self.metrics.tool_call_count += 1
132+
133+
def record_intercept(self) -> None:
134+
"""Increment the filter intercept counter."""
135+
self.metrics.intercept_count += 1
136+
137+
def record_findings(
138+
self,
139+
findings: list[Finding],
140+
warnings: Optional[list[Finding]] = None,
141+
needs_review: Optional[list[Finding]] = None,
142+
) -> None:
143+
"""Record findings and compute severity distribution.
144+
145+
Args:
146+
findings: High-confidence findings.
147+
warnings: Medium-confidence findings (optional).
148+
needs_review: Low-confidence findings (optional).
149+
"""
150+
all_findings = list(findings)
151+
if warnings:
152+
all_findings.extend(warnings)
153+
if needs_review:
154+
all_findings.extend(needs_review)
155+
156+
self.metrics.finding_count = len(all_findings)
157+
158+
# Severity distribution
159+
severity_counts: Counter = Counter()
160+
for f in all_findings:
161+
severity_counts[f.severity.value] += 1
162+
self.metrics.severity_counts = dict(severity_counts)
163+
164+
def record_exception(self, exception: Exception) -> None:
165+
"""Record an exception type for the audit log.
166+
167+
Args:
168+
exception: The exception that occurred.
169+
"""
170+
exc_type = type(exception).__name__
171+
if exc_type not in self.metrics.exception_types:
172+
self.metrics.exception_types.append(exc_type)
173+
self.metrics.errors.append(f"{exc_type}: {str(exception)[:200]}")
174+
175+
# ── Batch / convenience ──
176+
177+
def update_from_task(self, task: ReviewTask) -> None:
178+
"""Update metrics from a completed ReviewTask."""
179+
if task.total_duration_ms is not None:
180+
self.metrics.total_duration_ms = task.total_duration_ms
181+
if task.error_message:
182+
self.metrics.errors.append(task.error_message)
183+
184+
185+
# ──────────────────────────────────────────────
186+
# 7.3: DB persistence helper
187+
# ──────────────────────────────────────────────
188+
189+
190+
def save_monitor_summary(
191+
storage: StorageABC,
192+
task_id: str,
193+
total_duration_ms: float = 0.0,
194+
sandbox_duration_ms: float = 0.0,
195+
tool_call_count: int = 0,
196+
intercept_count: int = 0,
197+
finding_count: int = 0,
198+
severity_distribution: Optional[dict[str, int]] = None,
199+
exception_types: Optional[list[str]] = None,
200+
) -> MonitorSummary:
201+
"""Create and save a MonitorSummary to the database.
202+
203+
This is a convenience function for one-off saves without creating
204+
a full ReviewMonitor instance.
205+
206+
Args:
207+
storage: Storage backend.
208+
task_id: Review task ID.
209+
total_duration_ms: Total review duration in ms.
210+
sandbox_duration_ms: Total sandbox duration in ms.
211+
tool_call_count: Number of tool calls.
212+
intercept_count: Number of filter intercepts.
213+
finding_count: Total number of findings.
214+
severity_distribution: Dict of severity → count.
215+
exception_types: List of exception type names.
216+
217+
Returns:
218+
The saved MonitorSummary.
219+
"""
220+
summary = MonitorSummary(
221+
task_id=task_id,
222+
total_duration_ms=total_duration_ms,
223+
sandbox_duration_ms=sandbox_duration_ms,
224+
tool_call_count=tool_call_count,
225+
intercept_count=intercept_count,
226+
finding_count=finding_count,
227+
severity_distribution=json.dumps(severity_distribution or {}, ensure_ascii=False),
228+
exception_types=json.dumps(exception_types or [], ensure_ascii=False),
229+
)
230+
return storage.save_monitor_summary(summary)
231+
232+
233+
def get_monitor_summary(storage: StorageABC, task_id: str) -> Optional[MonitorSummary]:
234+
"""Retrieve a MonitorSummary from the database."""
235+
return storage.get_monitor_summary(task_id)

0 commit comments

Comments
 (0)