Skip to content

Commit b0acc9c

Browse files
committed
fix: review round 3 -- 8 fixes across observability.py
1. Double cache counting: record_search no longer tracks cache_hits/ cache_misses (cache.py already does via metrics.increment). get_metrics now reads from _counters. Eliminates double counting. 2. push_scope -> new_scope: 3 occurrences updated for sentry-sdk 2.x compatibility. Test warnings dropped from 54 to 44. 3. datetime.utcnow() -> datetime.now(timezone.utc): 3 occurrences. utcnow() deprecated in Python 3.12+. 4. Type annotations: added -> None to 13 functions (StructuredLogger methods + Sentry helpers) per CLAUDE.md. 5. deque for _timings: O(1) append instead of O(n) slice for cap. 6. get_metrics: avg_indexing_speed computed once, reused in summary. 7. _filter_events: merged duplicate if blocks into one. Skipped (3rd+ time): lazy singleton, contextvars, async/sync wrapper factoring. 284 tests pass. 10 fewer warnings.
1 parent d28c471 commit b0acc9c

1 file changed

Lines changed: 34 additions & 45 deletions

File tree

backend/services/observability.py

Lines changed: 34 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async def index_repo(repo_id: str):
2323
from typing import Optional, Any, Dict
2424
from functools import wraps
2525
from contextlib import contextmanager
26-
from datetime import datetime
26+
from datetime import datetime, timezone
2727
from collections import deque
2828

2929
# Environment
@@ -43,15 +43,15 @@ class StructuredLogger:
4343
logger.error("Failed to index", repo_id="xyz", error=str(e))
4444
"""
4545

46-
def __init__(self, name: str = "codeintel"):
46+
def __init__(self, name: str = "codeintel") -> None:
4747
self.name = name
4848
self.level = getattr(logging, LOG_LEVEL.upper(), logging.INFO)
4949
self._context: Dict[str, Any] = {}
5050

5151
def _format_message(self, level: str, message: str, **kwargs) -> str:
5252
"""Format log message based on environment"""
5353
data = {
54-
"timestamp": datetime.utcnow().isoformat(),
54+
"timestamp": datetime.now(timezone.utc).isoformat(),
5555
"level": level,
5656
"service": self.name,
5757
"message": message,
@@ -73,7 +73,7 @@ def _format_message(self, level: str, message: str, **kwargs) -> str:
7373
parts.append(extras)
7474
return " ".join(parts)
7575

76-
def _log(self, level: str, level_num: int, message: str, **kwargs):
76+
def _log(self, level: str, level_num: int, message: str, **kwargs) -> None:
7777
"""Internal log method"""
7878
if level_num < self.level:
7979
return
@@ -84,27 +84,27 @@ def _log(self, level: str, level_num: int, message: str, **kwargs):
8484
output = sys.stderr if level_num >= logging.ERROR else sys.stdout
8585
print(formatted, file=output)
8686

87-
def set_context(self, **kwargs):
87+
def set_context(self, **kwargs) -> None:
8888
"""Set persistent context for all subsequent logs"""
8989
self._context.update(kwargs)
9090

91-
def clear_context(self):
91+
def clear_context(self) -> None:
9292
"""Clear all context"""
9393
self._context = {}
9494

95-
def debug(self, message: str, **kwargs):
95+
def debug(self, message: str, **kwargs) -> None:
9696
self._log("DEBUG", logging.DEBUG, message, **kwargs)
9797

98-
def info(self, message: str, **kwargs):
98+
def info(self, message: str, **kwargs) -> None:
9999
self._log("INFO", logging.INFO, message, **kwargs)
100100

101-
def warning(self, message: str, **kwargs):
101+
def warning(self, message: str, **kwargs) -> None:
102102
self._log("WARNING", logging.WARNING, message, **kwargs)
103103

104-
def error(self, message: str, **kwargs):
104+
def error(self, message: str, **kwargs) -> None:
105105
self._log("ERROR", logging.ERROR, message, **kwargs)
106106

107-
def critical(self, message: str, **kwargs):
107+
def critical(self, message: str, **kwargs) -> None:
108108
self._log("CRITICAL", logging.CRITICAL, message, **kwargs)
109109

110110

@@ -114,7 +114,7 @@ def critical(self, message: str, **kwargs):
114114

115115
# SENTRY INTEGRATION HELPERS
116116

117-
def set_operation_context(operation: str, **kwargs):
117+
def set_operation_context(operation: str, **kwargs) -> None:
118118
"""
119119
Set Sentry context for current operation.
120120
@@ -135,7 +135,7 @@ def set_operation_context(operation: str, **kwargs):
135135
pass
136136

137137

138-
def add_breadcrumb(message: str, category: str = "custom", level: str = "info", **data):
138+
def add_breadcrumb(message: str, category: str = "custom", level: str = "info", **data) -> None:
139139
"""
140140
Add breadcrumb for Sentry error context.
141141
@@ -153,7 +153,7 @@ def add_breadcrumb(message: str, category: str = "custom", level: str = "info",
153153
pass
154154

155155

156-
def capture_exception(error: Exception, **context):
156+
def capture_exception(error: Exception, **context) -> None:
157157
"""
158158
Capture exception to Sentry with additional context.
159159
@@ -162,7 +162,7 @@ def capture_exception(error: Exception, **context):
162162
"""
163163
try:
164164
import sentry_sdk
165-
with sentry_sdk.push_scope() as scope:
165+
with sentry_sdk.new_scope() as scope:
166166
for key, value in context.items():
167167
scope.set_extra(key, value)
168168
sentry_sdk.capture_exception(error)
@@ -171,11 +171,11 @@ def capture_exception(error: Exception, **context):
171171
logger.error(f"Exception (no Sentry): {type(error).__name__}: {error}", **context)
172172

173173

174-
def capture_message(message: str, level: str = "info", **context):
174+
def capture_message(message: str, level: str = "info", **context) -> None:
175175
"""Capture a message (not exception) to Sentry"""
176176
try:
177177
import sentry_sdk
178-
with sentry_sdk.push_scope() as scope:
178+
with sentry_sdk.new_scope() as scope:
179179
for key, value in context.items():
180180
scope.set_extra(key, value)
181181
sentry_sdk.capture_message(message, level=level)
@@ -321,8 +321,6 @@ def __init__(self) -> None:
321321
self._indexing_times: deque = deque(maxlen=100)
322322
self._search_times: deque = deque(maxlen=100)
323323
self._total_searches: int = 0
324-
self._cache_hits: int = 0
325-
self._cache_misses: int = 0
326324

327325
def increment(self, name: str, value: int = 1, **tags) -> None:
328326
"""Increment a counter"""
@@ -331,10 +329,8 @@ def increment(self, name: str, value: int = 1, **tags) -> None:
331329
def timing(self, name: str, value_ms: float) -> None:
332330
"""Record a timing measurement"""
333331
if name not in self._timings:
334-
self._timings[name] = []
332+
self._timings[name] = deque(maxlen=1000)
335333
self._timings[name].append(value_ms)
336-
if len(self._timings[name]) > 1000:
337-
self._timings[name] = self._timings[name][-1000:]
338334

339335
def gauge(self, name: str, value: float) -> None:
340336
"""Record a point-in-time value"""
@@ -347,47 +343,46 @@ def record_indexing(self, repo_id: str, duration: float, function_count: int) ->
347343
"duration": duration,
348344
"function_count": function_count,
349345
"speed": function_count / duration if duration > 0 else 0,
350-
"timestamp": datetime.now().isoformat(),
346+
"timestamp": datetime.now(timezone.utc).isoformat(),
351347
})
352348

353349
def record_search(self, duration: float, cached: bool) -> None:
354350
"""Record search performance for dashboard metrics."""
355351
self._search_times.append({
356352
"duration": duration,
357353
"cached": cached,
358-
"timestamp": datetime.now().isoformat(),
354+
"timestamp": datetime.now(timezone.utc).isoformat(),
359355
})
360356
self._total_searches += 1
361-
if cached:
362-
self._cache_hits += 1
363-
else:
364-
self._cache_misses += 1
357+
# cache hit/miss counting handled by cache.py via metrics.increment()
358+
# to avoid double counting now that we're a single Metrics instance
365359

366360
def get_metrics(self) -> Dict:
367361
"""Dashboard-friendly performance summary (used by /health and /metrics)."""
368362
indexing_speeds = [m["speed"] for m in self._indexing_times]
369363
search_durations = [m["duration"] for m in self._search_times]
370-
cache_hit_rate = (
371-
(self._cache_hits / self._total_searches * 100)
372-
if self._total_searches > 0 else 0
364+
avg_indexing_speed = (
365+
sum(indexing_speeds) / len(indexing_speeds) if indexing_speeds else 0
373366
)
367+
# Cache hits/misses come from _counters (set by cache.py)
368+
cache_hits = self._counters.get("cache_hits", 0)
369+
cache_misses = self._counters.get("cache_misses", 0)
370+
cache_total = cache_hits + cache_misses
371+
cache_hit_rate = (cache_hits / cache_total * 100) if cache_total > 0 else 0
374372

375373
return {
376374
"indexing": {
377375
"total_operations": len(self._indexing_times),
378-
"avg_speed_functions_per_sec": (
379-
sum(indexing_speeds) / len(indexing_speeds)
380-
if indexing_speeds else 0
381-
),
376+
"avg_speed_functions_per_sec": avg_indexing_speed,
382377
"max_speed": max(indexing_speeds) if indexing_speeds else 0,
383378
"min_speed": min(indexing_speeds) if indexing_speeds else 0,
384379
"recent_operations": list(self._indexing_times)[-10:],
385380
},
386381
"search": {
387382
"total_searches": self._total_searches,
388383
"cache_hit_rate": f"{cache_hit_rate:.1f}%",
389-
"cache_hits": self._cache_hits,
390-
"cache_misses": self._cache_misses,
384+
"cache_hits": cache_hits,
385+
"cache_misses": cache_misses,
391386
"avg_duration_ms": (
392387
sum(search_durations) / len(search_durations) * 1000
393388
if search_durations else 0
@@ -398,10 +393,7 @@ def get_metrics(self) -> Dict:
398393
"health": "healthy",
399394
"cache_working": cache_hit_rate > 0,
400395
"indexing_performance": (
401-
"good" if (
402-
sum(indexing_speeds) / len(indexing_speeds)
403-
if indexing_speeds else 0
404-
) > 10 else "needs_improvement"
396+
"good" if avg_indexing_speed > 10 else "needs_improvement"
405397
),
406398
},
407399
}
@@ -431,8 +423,6 @@ def reset(self) -> None:
431423
self._indexing_times.clear()
432424
self._search_times.clear()
433425
self._total_searches = 0
434-
self._cache_hits = 0
435-
self._cache_misses = 0
436426

437427

438428
# SENTRY INITIALIZATION (moved from services/sentry.py)
@@ -496,7 +486,6 @@ def _filter_events(event: Dict[str, Any], hint: Optional[Dict[str, Any]]) -> Opt
496486
if any(path in exception_value for path in bot_paths):
497487
return None
498488

499-
if exception_values:
500489
exception_type = exception_values[0].get("type", "")
501490
if exception_type in ("RequestValidationError", "ValidationError"):
502491
return None
@@ -517,7 +506,7 @@ def capture_http_exception(request: Any, exc: Exception, status_code: int) -> No
517506
"""Capture HTTP exception with request context for Sentry."""
518507
try:
519508
import sentry_sdk
520-
with sentry_sdk.push_scope() as scope:
509+
with sentry_sdk.new_scope() as scope:
521510
scope.set_extra("status_code", status_code)
522511
scope.set_extra("path", str(request.url.path))
523512
scope.set_extra("method", request.method)

0 commit comments

Comments
 (0)