Coverage for src / lexigram / ui / performance / observability.py: 100%
57 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-10 04:11 +0800
1"""
2Observability utilities for Lexigram UI.
4Provides general-purpose in-memory metrics collection for HTMX components.
5HTMX-specific tracking, debug panels, and request logging are admin-specific
6and live in lexigram.admin.ui.observability.
7"""
9from __future__ import annotations
11from dataclasses import dataclass, field
12from enum import Enum
13from time import time
14from typing import Any
16# === Metrics Collection ===
19class MetricType(str, Enum):
20 """Types of metrics to collect."""
22 COUNTER = "counter"
23 HISTOGRAM = "histogram"
24 GAUGE = "gauge"
27@dataclass
28class MetricProtocol:
29 """A single metric data point."""
31 name: str
32 value: float
33 type: MetricType
34 labels: dict[str, str] = field(default_factory=dict)
35 timestamp: float = field(default_factory=time)
38class MetricsCollector:
39 """
40 Collect and track UI metrics.
42 This is a simple in-memory collector. For production,
43 integrate with Prometheus, StatsD, or similar.
44 """
46 def __init__(self) -> None:
47 self._counters: dict[str, float] = {}
48 self._histograms: dict[str, list[float]] = {}
49 self._gauges: dict[str, float] = {}
51 def _make_key(self, name: str, labels: dict[str, str] | None = None) -> str:
52 """Create metric key with labels."""
53 if not labels:
54 return name
55 sorted_labels = sorted(labels.items())
56 label_str = ",".join(f"{k}={v}" for k, v in sorted_labels)
57 return f"{name}{{{label_str}}}"
59 def inc(
60 self,
61 name: str,
62 value: float = 1.0,
63 labels: dict[str, str] | None = None,
64 ) -> None:
65 """Increment a counter."""
66 key = self._make_key(name, labels)
67 self._counters[key] = self._counters.get(key, 0) + value
69 def observe(
70 self,
71 name: str,
72 value: float,
73 labels: dict[str, str] | None = None,
74 ) -> None:
75 """Record a histogram observation."""
76 key = self._make_key(name, labels)
77 if key not in self._histograms:
78 self._histograms[key] = []
79 self._histograms[key].append(value)
81 def set(
82 self,
83 name: str,
84 value: float,
85 labels: dict[str, str] | None = None,
86 ) -> None:
87 """Set a gauge value."""
88 key = self._make_key(name, labels)
89 self._gauges[key] = value
91 def get_counter(self, name: str, labels: dict[str, str] | None = None) -> float:
92 """Get counter value."""
93 key = self._make_key(name, labels)
94 return self._counters.get(key, 0)
96 def get_histogram_stats(
97 self,
98 name: str,
99 labels: dict[str, str] | None = None,
100 ) -> dict[str, float]:
101 """Get histogram statistics."""
102 key = self._make_key(name, labels)
103 values = self._histograms.get(key, [])
104 if not values:
105 return {"count": 0, "sum": 0, "avg": 0, "min": 0, "max": 0}
107 return {
108 "count": len(values),
109 "sum": sum(values),
110 "avg": sum(values) / len(values),
111 "min": min(values),
112 "max": max(values),
113 }
115 def get_gauge(self, name: str, labels: dict[str, str] | None = None) -> float:
116 """Get gauge value."""
117 key = self._make_key(name, labels)
118 return self._gauges.get(key, 0)
120 def reset(self) -> None:
121 """Reset all metrics."""
122 self._counters.clear()
123 self._histograms.clear()
124 self._gauges.clear()
126 def to_dict(self) -> dict[str, Any]:
127 """Export all metrics as dictionary."""
128 return {
129 "counters": dict(self._counters),
130 "histograms": {
131 k: self.get_histogram_stats(k.split("{")[0]) for k in self._histograms
132 },
133 "gauges": dict(self._gauges),
134 }
137__all__ = ["MetricProtocol", "MetricType", "MetricsCollector"]