fleet-memory/hindsight-api/hindsight_api/metrics.py
Nicolò Boschi 3e72984cd2 chunks
2025-11-29 16:34:13 +01:00

227 lines
7.1 KiB
Python

"""
OpenTelemetry metrics instrumentation for Hindsight API.
This module provides metrics for:
- Operation latency (retain, recall, reflect) with percentiles
- Token usage (input/output) per operation
- Per-bank granularity via labels
"""
import logging
from typing import Dict, Any, Optional
from contextlib import contextmanager
import time
from opentelemetry import metrics
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.prometheus import PrometheusMetricReader
from prometheus_client import REGISTRY
logger = logging.getLogger(__name__)
# Global meter instance
_meter = None
def initialize_metrics(service_name: str = "hindsight-api", service_version: str = "1.0.0"):
"""
Initialize OpenTelemetry metrics with Prometheus exporter.
This should be called once during application startup.
Args:
service_name: Name of the service for resource attributes
service_version: Version of the service
Returns:
PrometheusMetricReader instance (for accessing metrics endpoint)
"""
global _meter
# Create resource with service information
resource = Resource.create({
"service.name": service_name,
"service.version": service_version,
})
# Create Prometheus metric reader
prometheus_reader = PrometheusMetricReader()
# Create meter provider with Prometheus exporter
provider = MeterProvider(
resource=resource,
metric_readers=[prometheus_reader]
)
# Set the global meter provider
metrics.set_meter_provider(provider)
# Get meter for this application
_meter = metrics.get_meter(__name__)
return prometheus_reader
def get_meter():
"""Get the global meter instance."""
if _meter is None:
raise RuntimeError("Metrics not initialized. Call initialize_metrics() first.")
return _meter
class MetricsCollectorBase:
"""Base class for metrics collectors."""
@contextmanager
def record_operation(self, operation: str, bank_id: str, budget: Optional[str] = None, max_tokens: Optional[int] = None):
"""Context manager to record operation duration and status."""
raise NotImplementedError
def record_tokens(self, operation: str, bank_id: str, input_tokens: int = 0, output_tokens: int = 0, budget: Optional[str] = None, max_tokens: Optional[int] = None):
"""Record token usage for an operation."""
raise NotImplementedError
class NoOpMetricsCollector(MetricsCollectorBase):
"""No-op metrics collector that does nothing. Used when metrics are disabled."""
@contextmanager
def record_operation(self, operation: str, bank_id: str, budget: Optional[str] = None, max_tokens: Optional[int] = None):
"""No-op context manager."""
yield
def record_tokens(self, operation: str, bank_id: str, input_tokens: int = 0, output_tokens: int = 0, budget: Optional[str] = None, max_tokens: Optional[int] = None):
"""No-op token recording."""
pass
class MetricsCollector(MetricsCollectorBase):
"""
Collector for Hindsight API metrics.
Provides methods to record latency and token usage for operations.
"""
def __init__(self):
self.meter = get_meter()
# Operation latency histogram (in seconds)
# Records duration of retain, recall, reflect operations
self.operation_duration = self.meter.create_histogram(
name="hindsight.operation.duration",
description="Duration of Hindsight operations in seconds",
unit="s"
)
# Token usage counters
self.tokens_input = self.meter.create_counter(
name="hindsight.tokens.input",
description="Number of input tokens consumed",
unit="tokens"
)
self.tokens_output = self.meter.create_counter(
name="hindsight.tokens.output",
description="Number of output tokens generated",
unit="tokens"
)
# Operation counter (success/failure)
self.operation_total = self.meter.create_counter(
name="hindsight.operation.total",
description="Total number of operations executed",
unit="operations"
)
@contextmanager
def record_operation(self, operation: str, bank_id: str, budget: Optional[str] = None, max_tokens: Optional[int] = None):
"""
Context manager to record operation duration and status.
Usage:
with metrics.record_operation("recall", bank_id="user123", budget="mid", max_tokens=4096):
# ... perform operation
pass
Args:
operation: Operation name (retain, recall, reflect)
bank_id: Memory bank ID
budget: Optional budget level (low, mid, high)
max_tokens: Optional max tokens for the operation
"""
start_time = time.time()
attributes = {
"operation": operation,
"bank_id": bank_id,
}
if budget:
attributes["budget"] = budget
if max_tokens:
attributes["max_tokens"] = str(max_tokens)
success = True
try:
yield
except Exception:
success = False
raise
finally:
duration = time.time() - start_time
attributes["success"] = str(success).lower()
# Record duration
self.operation_duration.record(duration, attributes)
# Record operation count
self.operation_total.add(1, attributes)
def record_tokens(self, operation: str, bank_id: str, input_tokens: int = 0, output_tokens: int = 0, budget: Optional[str] = None, max_tokens: Optional[int] = None):
"""
Record token usage for an operation.
Args:
operation: Operation name (retain, recall, reflect)
bank_id: Memory bank ID
input_tokens: Number of input tokens
output_tokens: Number of output tokens
budget: Optional budget level
max_tokens: Optional max tokens for the operation
"""
attributes = {
"operation": operation,
"bank_id": bank_id,
}
if budget:
attributes["budget"] = budget
if max_tokens:
attributes["max_tokens"] = str(max_tokens)
if input_tokens > 0:
self.tokens_input.add(input_tokens, attributes)
if output_tokens > 0:
self.tokens_output.add(output_tokens, attributes)
# Global metrics collector instance (defaults to no-op)
_metrics_collector: MetricsCollectorBase = NoOpMetricsCollector()
def get_metrics_collector() -> MetricsCollectorBase:
"""
Get the global metrics collector instance.
Returns a no-op collector if metrics are not initialized.
"""
return _metrics_collector
def create_metrics_collector() -> MetricsCollector:
"""
Create and set the global metrics collector.
Should be called after initialize_metrics().
"""
global _metrics_collector
_metrics_collector = MetricsCollector()
return _metrics_collector