Source code for iamai.observability
"""Runtime audit logging and in-memory counters for iamai."""
from __future__ import annotations
import json
from collections import defaultdict
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import Any
from loguru import logger
@dataclass(frozen=True, slots=True)
class MetricSeries:
"""One metric counter series with labels and value."""
name: str
labels: tuple[tuple[str, str], ...]
value: int
def formatted_name(self) -> str:
"""Return the metric name with labels rendered in Prometheus style."""
if not self.labels:
return self.name
label_text = ",".join(f"{key}={value}" for key, value in self.labels)
return f"{self.name}{{{label_text}}}"
def to_dict(self) -> dict[str, Any]:
"""Return this metric series as a JSON-compatible dictionary."""
return {
"name": self.name,
"labels": dict(self.labels),
"value": self.value,
}
[docs]
class RuntimeMetrics:
"""Small in-memory counter store for health checks and operator inspection."""
def __init__(self) -> None:
self._counters: dict[tuple[str, tuple[tuple[str, str], ...]], int] = defaultdict(int)
[docs]
def increment(self, name: str, value: int = 1, **labels: Any) -> None:
"""Increment a labeled counter by ``value``."""
normalized_name = str(name).strip()
if not normalized_name:
raise ValueError("metric name cannot be empty")
label_items = tuple(sorted((str(key), str(item)) for key, item in labels.items()))
self._counters[(normalized_name, label_items)] += int(value)
[docs]
def snapshot(self) -> dict[str, int]:
"""Return counters keyed by their formatted metric names."""
return {
MetricSeries(name=name, labels=labels, value=value).formatted_name(): value
for (name, labels), value in sorted(self._counters.items())
}
[docs]
def series(self) -> list[MetricSeries]:
"""Return all metric series sorted for deterministic output."""
return [
MetricSeries(name=name, labels=labels, value=value)
for (name, labels), value in sorted(self._counters.items())
]
[docs]
class AuditLogger:
"""Structured audit logger that emits one JSON object per runtime event."""
def __init__(self, logger_name: str = "iamai.audit") -> None:
self.logger_name = logger_name
self._logger = logger.bind(name=logger_name, audit=True)
[docs]
def emit(
self,
action: str,
*,
outcome: str = "ok",
level: int | str = "INFO",
**fields: Any,
) -> None:
"""Emit one structured audit record."""
payload = {
"ts": datetime.now(timezone.utc).isoformat(),
"action": str(action),
"outcome": str(outcome),
}
for key, value in fields.items():
if value is None:
continue
payload[str(key)] = value
self._logger.log(level, json.dumps(payload, ensure_ascii=False, sort_keys=True))