File size: 6,865 Bytes
9d29c62 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 | # domain/telemetry.py - V7.2 Observability Layer
"""
BuddyMath V7.2 Runtime Telemetry
Emits structured log-based metrics compatible with Datadog/Grafana log pipelines.
All metrics are emitted as structured JSON log lines on the logger named "buddymath.metrics".
DevOps should configure a log-based metric parser for lines containing "METRIC_EVENT".
Metric naming convention: <component>.<event_name>
"""
import logging
import json
import time
from datetime import datetime, timezone
from typing import Optional
metrics_logger = logging.getLogger("buddymath.metrics")
# ==================== METRIC KEY CONSTANTS ====================
# Use these constants everywhere to prevent typos and enable grep-ability.
class M:
"""V7.2 Metric Key Registry"""
# Core runtime outcome (feeds the Pie Chart)
RUNTIME_OUTCOME = "runtime.outcome" # values: success | hint_mode | planner_retry | leakage_fail | placeholder_fail | crs_block
# Fail Closed Rate (derived from RUNTIME_OUTCOME != success)
FAIL_CLOSED = "fail_closed" # emitted on any non-success outcome
# Planner (LLM #1)
PLANNER_RETRY = "planner.retry.count" # emitted on each retry attempt
PLANNER_ENUM_VIOLATION = "planner.enum_violation" # emitted when ComputeAction enum is violated
PLANNER_JSON_ERROR = "planner.json_error" # emitted on JSON parse / boundary failure
PLANNER_STRATEGY_DIST = "planner.strategy_distribution" # which Enum actions are chosen (drift detection)
# Renderer (LLM #2)
RENDERER_LEAKAGE_FAIL = "renderer.leakage_fail" # Whitelist scan failed
RENDERER_PLACEHOLDER_FAIL = "renderer.placeholder_violation" # Missing or invented placeholder
RENDERER_LEAKAGE_CHARS = "renderer.leakage_chars" # Offending chars (for slicing)
# Solver / Server Determinism
SOLVER_EXECUTION_MS = "solver.execution_time_ms" # Math engine wall-clock time
SIGNATURE_HASH_COLLISION = "signature.hash_collision" # MUST always be 0
# CRS Pre-Flight
CRS_BLOCK = "crs.preflight_block" # Emitted when CRS > 0.7 blocks LLM call
CRS_VALUE = "crs.value" # Raw CRS score for histogram
# Security
SUSPICIOUS_INPUT = "suspicious.input_pattern" # Potential prompt injection detected
# Pedagogical Diversity (V7.2.5)
PEDAGOGICAL_DRIFT = "pedagogical.drift_score" # Narrative laziness: > 0.35 = same template always chosen
# ==================== EMITTER ====================
def emit(metric: str, value, tags: Optional[dict] = None):
"""
Emit a single metric event as a structured JSON log line.
Datadog/Grafana log-based metric pipelines should parse lines with 'METRIC_EVENT'.
Format:
{"event": "METRIC_EVENT", "metric": "<name>", "value": <v>, "tags": {...}, "timestamp": "..."}
"""
payload = {
"event": "METRIC_EVENT",
"metric": metric,
"value": value,
"tags": tags or {},
"timestamp": datetime.now(timezone.utc).isoformat()
}
metrics_logger.info(json.dumps(payload, ensure_ascii=False))
def emit_runtime_outcome(outcome: str, problem_id: str = "unknown", grade: str = "unknown"):
"""Feeds the main Pie Chart and Fail Closed Rate gauge."""
emit(M.RUNTIME_OUTCOME, outcome, {"problem_id": problem_id, "grade": grade})
if outcome != "success":
emit(M.FAIL_CLOSED, 1, {"reason": outcome})
def emit_planner_retry(attempt: int, reason: str):
emit(M.PLANNER_RETRY, attempt, {"reason": reason})
def emit_planner_error(error_type: str, details: str = ""):
"""error_type: 'enum_violation' | 'json_error'"""
key = M.PLANNER_ENUM_VIOLATION if error_type == "enum_violation" else M.PLANNER_JSON_ERROR
emit(key, 1, {"details": details[:120]})
def emit_planner_strategy_distribution(actions: list):
"""
Phase 1 Live: Emit one metric event per chosen Enum action.
Feeds the planner.strategy_distribution dashboard panel.
A sudden shift in distribution signals Model Drift.
Example: [SOLVE_EQUATION, SIMPLIFY, SOLVE_EQUATION] → 3 events
"""
for action in actions:
emit(M.PLANNER_STRATEGY_DIST, 1, {"action": str(action)})
def emit_renderer_leakage(offending_chars: str):
emit(M.RENDERER_LEAKAGE_FAIL, 1, {"offending_chars": offending_chars[:50]})
emit(M.RENDERER_LEAKAGE_CHARS, offending_chars[:50])
def emit_renderer_placeholder_violation(violation_type: str, missing_id: str = ""):
"""violation_type: 'missing' | 'invented'"""
emit(M.RENDERER_PLACEHOLDER_FAIL, 1, {"type": violation_type, "id": missing_id})
def emit_solver_timing(start_time: float):
"""Call with time.time() snapshot taken BEFORE solver runs."""
elapsed_ms = round((time.time() - start_time) * 1000, 2)
emit(M.SOLVER_EXECUTION_MS, elapsed_ms)
return elapsed_ms
def emit_hash_collision(step_id: str, problem_id: str):
"""
CRITICAL: This MUST never be emitted in a correct system.
If it fires, it means two different expressions produced the same hash → P0 alert.
"""
metrics_logger.critical(
json.dumps({
"event": "METRIC_EVENT",
"metric": M.SIGNATURE_HASH_COLLISION,
"value": 1,
"tags": {"step_id": step_id, "problem_id": problem_id},
"timestamp": datetime.now(timezone.utc).isoformat(),
"severity": "P0_CRITICAL"
})
)
def emit_crs_block(crs_value: float, problem_id: str = "unknown"):
emit(M.CRS_BLOCK, 1, {"crs": crs_value, "problem_id": problem_id})
emit(M.CRS_VALUE, crs_value)
def emit_crs_value(crs_value: float):
emit(M.CRS_VALUE, crs_value)
def emit_suspicious_input(pattern: str, problem_id: str = "unknown"):
emit(M.SUSPICIOUS_INPUT, 1, {"pattern": pattern[:80], "problem_id": problem_id})
def emit_pedagogical_drift(concept_tag: str, drift_score: float):
"""
V7.2.5: Emitted when DiversityEngine detects narrative laziness.
drift_score > 0.35 means one template variant is dominating selection.
Alert DevOps: Renderer is repeating the same pedagogical phrasing — student experience degrades.
"""
emit(M.PEDAGOGICAL_DRIFT, round(drift_score, 3), {"concept": concept_tag})
# ==================== TIMER CONTEXT MANAGER ====================
class SolverTimer:
"""
Context manager for measuring Math Engine execution time.
Usage:
with SolverTimer() as t:
result = sympy_solve(...)
print(t.elapsed_ms)
"""
def __enter__(self):
self._start = time.time()
return self
def __exit__(self, *args):
self.elapsed_ms = round((time.time() - self._start) * 1000, 2)
emit(M.SOLVER_EXECUTION_MS, self.elapsed_ms)
|