Source code for structum_lab.plugins.observability.metrics
# src/structum_lab.plugins.observability/metrics.py
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: 2025 PythonWoods
"""In-Memory Metrics Collection.
Provides a simple in-memory metrics collector for development and testing,
along with a `track_operation` decorator for automatic timing.
"""
import logging
from collections.abc import Callable
from functools import wraps
from time import time
from typing import Any, TypeVar
T = TypeVar("T")
[docs]
class MetricsCollector:
"""Raccolta metriche in-memory (simil-Prometheus/StatsD)."""
[docs]
def __init__(self) -> None:
"""Initialize the metrics collector with empty storage."""
self._counters: dict[str, int] = {}
self._histograms: dict[str, list[Any]] = {}
self.logger = logging.getLogger("metrics")
def _build_key(self, metric: str, labels: dict[str, str]) -> str:
"""Build a unique key for a metric with labels."""
if not labels:
return metric
label_str = ",".join(f"{k}={v}" for k, v in sorted(labels.items()))
return f"{metric}{{{label_str}}}"
[docs]
def increment(self, metric: str, labels: dict[str, str] | None = None) -> None:
"""Increment a counter metric."""
key = self._build_key(metric, labels or {})
self._counters[key] = self._counters.get(key, 0) + 1
[docs]
def observe(self, metric: str, value: float, labels: dict[str, str] | None = None) -> None:
"""Record an observation value (e.g., duration)."""
key = self._build_key(metric, labels or {})
if key not in self._histograms:
self._histograms[key] = []
self._histograms[key].append(value)
[docs]
def get_counter(self, metric: str, labels: dict[str, str] | None = None) -> int:
"""Get the current value of a counter (for introspection)."""
key = self._build_key(metric, labels or {})
return self._counters.get(key, 0)
# Global instance for the plugin
metrics = MetricsCollector()
[docs]
def track_operation(operation_name: str):
"""Decorator per tracciare durata e successo/errore di un'operazione."""
def decorator(func: Callable[..., T]) -> Callable[..., T]:
@wraps(func)
def wrapper(*args, **kwargs) -> T:
start = time()
status = "success"
error_type = "None"
try:
result = func(*args, **kwargs)
return result
except Exception as e:
status = "error"
error_type = type(e).__name__
raise
finally:
duration = time() - start
labels = {
"operation": operation_name,
"status": status,
"error": error_type,
}
# Track total operations
metrics.increment("operation_total", labels)
# Track duration
metrics.observe(
"operation_duration_seconds",
duration,
{"operation": operation_name},
)
return wrapper
return decorator