| 333 | |
| 334 | |
| 335 | class TraceStore: |
| 336 | def __init__( |
| 337 | self, |
| 338 | storage: TraceStorage | None = None, |
| 339 | now_fn: Any = None, |
| 340 | *, |
| 341 | hot_days: int | None = None, |
| 342 | ) -> None: |
| 343 | if storage is None: |
| 344 | target_dir = data_dir() / "traces" |
| 345 | legacy = data_dir() / "traces.json" |
| 346 | try: |
| 347 | migrate_legacy_json(legacy, target_dir) |
| 348 | except Exception: |
| 349 | pass |
| 350 | storage = FileTraceStorage(base_dir=target_dir) |
| 351 | self._storage = storage |
| 352 | self._now = now_fn or time.time |
| 353 | if hot_days is None: |
| 354 | try: |
| 355 | hot_days = int(os.environ.get("UNCOMMON_ROUTE_TRACE_HOT_DAYS", "2")) |
| 356 | except Exception: |
| 357 | hot_days = 2 |
| 358 | self._hot_days = max(1, hot_days) |
| 359 | self._records: list[RequestTrace] = [] |
| 360 | self._load() |
| 361 | |
| 362 | @property |
| 363 | def count(self) -> int: |
| 364 | return len(self._records) |
| 365 | |
| 366 | def record(self, trace: RequestTrace) -> None: |
| 367 | trace.tier = _normalize_tier_label(trace.tier) |
| 368 | trace.decision_tier = _normalize_tier_label(trace.decision_tier) if trace.decision_tier else "" |
| 369 | trace.served_quality = _normalize_served_quality(trace.served_quality) |
| 370 | trace.served_quality_target = _normalize_served_quality(trace.served_quality_target) |
| 371 | trace.served_quality_floor = _normalize_served_quality(trace.served_quality_floor) |
| 372 | trace.capability_lane = str(trace.capability_lane or "").strip().lower() |
| 373 | trace.feedback_from_tier = _normalize_tier_label(trace.feedback_from_tier) if trace.feedback_from_tier else "" |
| 374 | trace.feedback_to_tier = _normalize_tier_label(trace.feedback_to_tier) if trace.feedback_to_tier else "" |
| 375 | |
| 376 | # Persist full row (incl. cold fields) to disk. |
| 377 | payload = _trace_payload(trace) |
| 378 | try: |
| 379 | self._storage.append({"type": "trace", **payload}) |
| 380 | except Exception: |
| 381 | pass |
| 382 | |
| 383 | # Hot copy: drop cold fields before keeping in memory. |
| 384 | for f in COLD_FIELDS: |
| 385 | setattr(trace, f, _empty_for(f)) |
| 386 | self._records.append(trace) |
| 387 | self._cleanup() |
| 388 | |
| 389 | def record_feedback( |
| 390 | self, |
| 391 | request_id: str, |
| 392 | *, |
no outgoing calls