proxilion 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- proxilion/__init__.py +136 -0
- proxilion/audit/__init__.py +133 -0
- proxilion/audit/base_exporters.py +527 -0
- proxilion/audit/compliance/__init__.py +130 -0
- proxilion/audit/compliance/base.py +457 -0
- proxilion/audit/compliance/eu_ai_act.py +603 -0
- proxilion/audit/compliance/iso27001.py +544 -0
- proxilion/audit/compliance/soc2.py +491 -0
- proxilion/audit/events.py +493 -0
- proxilion/audit/explainability.py +1173 -0
- proxilion/audit/exporters/__init__.py +58 -0
- proxilion/audit/exporters/aws_s3.py +636 -0
- proxilion/audit/exporters/azure_storage.py +608 -0
- proxilion/audit/exporters/cloud_base.py +468 -0
- proxilion/audit/exporters/gcp_storage.py +570 -0
- proxilion/audit/exporters/multi_exporter.py +498 -0
- proxilion/audit/hash_chain.py +652 -0
- proxilion/audit/logger.py +543 -0
- proxilion/caching/__init__.py +49 -0
- proxilion/caching/tool_cache.py +633 -0
- proxilion/context/__init__.py +73 -0
- proxilion/context/context_window.py +556 -0
- proxilion/context/message_history.py +505 -0
- proxilion/context/session.py +735 -0
- proxilion/contrib/__init__.py +51 -0
- proxilion/contrib/anthropic.py +609 -0
- proxilion/contrib/google.py +1012 -0
- proxilion/contrib/langchain.py +641 -0
- proxilion/contrib/mcp.py +893 -0
- proxilion/contrib/openai.py +646 -0
- proxilion/core.py +3058 -0
- proxilion/decorators.py +966 -0
- proxilion/engines/__init__.py +287 -0
- proxilion/engines/base.py +266 -0
- proxilion/engines/casbin_engine.py +412 -0
- proxilion/engines/opa_engine.py +493 -0
- proxilion/engines/simple.py +437 -0
- proxilion/exceptions.py +887 -0
- proxilion/guards/__init__.py +54 -0
- proxilion/guards/input_guard.py +522 -0
- proxilion/guards/output_guard.py +634 -0
- proxilion/observability/__init__.py +198 -0
- proxilion/observability/cost_tracker.py +866 -0
- proxilion/observability/hooks.py +683 -0
- proxilion/observability/metrics.py +798 -0
- proxilion/observability/session_cost_tracker.py +1063 -0
- proxilion/policies/__init__.py +67 -0
- proxilion/policies/base.py +304 -0
- proxilion/policies/builtin.py +486 -0
- proxilion/policies/registry.py +376 -0
- proxilion/providers/__init__.py +201 -0
- proxilion/providers/adapter.py +468 -0
- proxilion/providers/anthropic_adapter.py +330 -0
- proxilion/providers/gemini_adapter.py +391 -0
- proxilion/providers/openai_adapter.py +294 -0
- proxilion/py.typed +0 -0
- proxilion/resilience/__init__.py +81 -0
- proxilion/resilience/degradation.py +615 -0
- proxilion/resilience/fallback.py +555 -0
- proxilion/resilience/retry.py +554 -0
- proxilion/scheduling/__init__.py +57 -0
- proxilion/scheduling/priority_queue.py +419 -0
- proxilion/scheduling/scheduler.py +459 -0
- proxilion/security/__init__.py +244 -0
- proxilion/security/agent_trust.py +968 -0
- proxilion/security/behavioral_drift.py +794 -0
- proxilion/security/cascade_protection.py +869 -0
- proxilion/security/circuit_breaker.py +428 -0
- proxilion/security/cost_limiter.py +690 -0
- proxilion/security/idor_protection.py +460 -0
- proxilion/security/intent_capsule.py +849 -0
- proxilion/security/intent_validator.py +495 -0
- proxilion/security/memory_integrity.py +767 -0
- proxilion/security/rate_limiter.py +509 -0
- proxilion/security/scope_enforcer.py +680 -0
- proxilion/security/sequence_validator.py +636 -0
- proxilion/security/trust_boundaries.py +784 -0
- proxilion/streaming/__init__.py +70 -0
- proxilion/streaming/detector.py +761 -0
- proxilion/streaming/transformer.py +674 -0
- proxilion/timeouts/__init__.py +55 -0
- proxilion/timeouts/decorators.py +477 -0
- proxilion/timeouts/manager.py +545 -0
- proxilion/tools/__init__.py +69 -0
- proxilion/tools/decorators.py +493 -0
- proxilion/tools/registry.py +732 -0
- proxilion/types.py +339 -0
- proxilion/validation/__init__.py +93 -0
- proxilion/validation/pydantic_schema.py +351 -0
- proxilion/validation/schema.py +651 -0
- proxilion-0.0.1.dist-info/METADATA +872 -0
- proxilion-0.0.1.dist-info/RECORD +94 -0
- proxilion-0.0.1.dist-info/WHEEL +4 -0
- proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Metrics and observability hooks for Proxilion.
|
|
3
|
+
|
|
4
|
+
Provides hooks for metrics and observability without requiring external
|
|
5
|
+
dependencies. Allows integration with Prometheus, OpenTelemetry, StatsD, etc.
|
|
6
|
+
|
|
7
|
+
Quick Start:
|
|
8
|
+
>>> from proxilion.observability import (
|
|
9
|
+
... ObservabilityHooks,
|
|
10
|
+
... InMemoryMetricHook,
|
|
11
|
+
... emit_counter,
|
|
12
|
+
... emit_timing,
|
|
13
|
+
... )
|
|
14
|
+
>>>
|
|
15
|
+
>>> # Set up metrics hook
|
|
16
|
+
>>> hooks = ObservabilityHooks.get_instance()
|
|
17
|
+
>>> memory_hook = InMemoryMetricHook()
|
|
18
|
+
>>> hooks.add_metric_hook(memory_hook)
|
|
19
|
+
>>>
|
|
20
|
+
>>> # Emit metrics
|
|
21
|
+
>>> emit_counter("proxilion.auth.requests", tags={"user": "alice"})
|
|
22
|
+
>>> emit_timing("proxilion.auth.latency_ms", 45.2, tags={"tool": "search"})
|
|
23
|
+
>>>
|
|
24
|
+
>>> # Check recorded metrics
|
|
25
|
+
>>> memory_hook.get_counter("proxilion.auth.requests", {"user": "alice"})
|
|
26
|
+
1.0
|
|
27
|
+
|
|
28
|
+
Integration with Prometheus:
|
|
29
|
+
>>> from prometheus_client import Counter, Histogram
|
|
30
|
+
>>>
|
|
31
|
+
>>> class PrometheusMetricHook:
|
|
32
|
+
... def __init__(self):
|
|
33
|
+
... self.auth_requests = Counter(
|
|
34
|
+
... "proxilion_auth_requests_total",
|
|
35
|
+
... "Total authorization requests",
|
|
36
|
+
... ["user", "tool"]
|
|
37
|
+
... )
|
|
38
|
+
... self.auth_latency = Histogram(
|
|
39
|
+
... "proxilion_auth_latency_seconds",
|
|
40
|
+
... "Authorization latency in seconds"
|
|
41
|
+
... )
|
|
42
|
+
...
|
|
43
|
+
... def increment(self, name, value=1.0, tags=None):
|
|
44
|
+
... if name == "proxilion.auth.requests":
|
|
45
|
+
... self.auth_requests.labels(**(tags or {})).inc(value)
|
|
46
|
+
...
|
|
47
|
+
... def histogram(self, name, value, tags=None):
|
|
48
|
+
... if name == "proxilion.auth.latency_ms":
|
|
49
|
+
... self.auth_latency.observe(value / 1000) # Convert to seconds
|
|
50
|
+
...
|
|
51
|
+
... def gauge(self, name, value, tags=None): pass
|
|
52
|
+
... def timing(self, name, duration_ms, tags=None):
|
|
53
|
+
... self.histogram(name, duration_ms, tags)
|
|
54
|
+
>>>
|
|
55
|
+
>>> hooks = ObservabilityHooks.get_instance()
|
|
56
|
+
>>> hooks.add_metric_hook(PrometheusMetricHook())
|
|
57
|
+
|
|
58
|
+
Integration with OpenTelemetry:
|
|
59
|
+
>>> from opentelemetry import metrics
|
|
60
|
+
>>>
|
|
61
|
+
>>> class OpenTelemetryMetricHook:
|
|
62
|
+
... def __init__(self):
|
|
63
|
+
... meter = metrics.get_meter("proxilion")
|
|
64
|
+
... self._counters = {}
|
|
65
|
+
... self._histograms = {}
|
|
66
|
+
... self._meter = meter
|
|
67
|
+
...
|
|
68
|
+
... def increment(self, name, value=1.0, tags=None):
|
|
69
|
+
... if name not in self._counters:
|
|
70
|
+
... self._counters[name] = self._meter.create_counter(name)
|
|
71
|
+
... self._counters[name].add(value, tags or {})
|
|
72
|
+
...
|
|
73
|
+
... def histogram(self, name, value, tags=None):
|
|
74
|
+
... if name not in self._histograms:
|
|
75
|
+
... self._histograms[name] = self._meter.create_histogram(name)
|
|
76
|
+
... self._histograms[name].record(value, tags or {})
|
|
77
|
+
...
|
|
78
|
+
... def gauge(self, name, value, tags=None): pass
|
|
79
|
+
... def timing(self, name, duration_ms, tags=None):
|
|
80
|
+
... self.histogram(name, duration_ms, tags)
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
from __future__ import annotations
|
|
84
|
+
|
|
85
|
+
import logging
|
|
86
|
+
from collections import defaultdict
|
|
87
|
+
from dataclasses import dataclass
|
|
88
|
+
from enum import Enum
|
|
89
|
+
from typing import Any, Protocol, runtime_checkable
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class MetricType(Enum):
|
|
93
|
+
"""Types of metrics that can be emitted."""
|
|
94
|
+
|
|
95
|
+
COUNTER = "counter"
|
|
96
|
+
"""A monotonically increasing counter."""
|
|
97
|
+
|
|
98
|
+
GAUGE = "gauge"
|
|
99
|
+
"""A value that can go up or down."""
|
|
100
|
+
|
|
101
|
+
HISTOGRAM = "histogram"
|
|
102
|
+
"""Distribution of values."""
|
|
103
|
+
|
|
104
|
+
SUMMARY = "summary"
|
|
105
|
+
"""Similar to histogram but calculates quantiles."""
|
|
106
|
+
|
|
107
|
+
TIMING = "timing"
|
|
108
|
+
"""Duration measurement (usually milliseconds)."""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@runtime_checkable
|
|
112
|
+
class MetricHook(Protocol):
|
|
113
|
+
"""
|
|
114
|
+
Protocol for metric backends (Prometheus, StatsD, OpenTelemetry, etc.).
|
|
115
|
+
|
|
116
|
+
Implement this protocol to integrate with your metrics infrastructure.
|
|
117
|
+
The hooks will be called by Proxilion whenever metrics are emitted.
|
|
118
|
+
|
|
119
|
+
Example:
|
|
120
|
+
>>> class MyMetricHook:
|
|
121
|
+
... def increment(self, name, value=1.0, tags=None):
|
|
122
|
+
... # Send to your metrics backend
|
|
123
|
+
... pass
|
|
124
|
+
...
|
|
125
|
+
... def gauge(self, name, value, tags=None):
|
|
126
|
+
... pass
|
|
127
|
+
...
|
|
128
|
+
... def histogram(self, name, value, tags=None):
|
|
129
|
+
... pass
|
|
130
|
+
...
|
|
131
|
+
... def timing(self, name, duration_ms, tags=None):
|
|
132
|
+
... pass
|
|
133
|
+
>>>
|
|
134
|
+
>>> from proxilion.observability import ObservabilityHooks
|
|
135
|
+
>>> hooks = ObservabilityHooks.get_instance()
|
|
136
|
+
>>> hooks.add_metric_hook(MyMetricHook())
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
def increment(self, name: str, value: float = 1.0, tags: dict[str, Any] | None = None) -> None:
|
|
140
|
+
"""
|
|
141
|
+
Increment a counter metric.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
name: The metric name (e.g., "proxilion.auth.requests").
|
|
145
|
+
value: The amount to increment by (default 1.0).
|
|
146
|
+
tags: Optional tags/labels for the metric.
|
|
147
|
+
"""
|
|
148
|
+
...
|
|
149
|
+
|
|
150
|
+
def gauge(self, name: str, value: float, tags: dict[str, Any] | None = None) -> None:
|
|
151
|
+
"""
|
|
152
|
+
Set a gauge metric.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
name: The metric name.
|
|
156
|
+
value: The gauge value.
|
|
157
|
+
tags: Optional tags/labels for the metric.
|
|
158
|
+
"""
|
|
159
|
+
...
|
|
160
|
+
|
|
161
|
+
def histogram(self, name: str, value: float, tags: dict[str, Any] | None = None) -> None:
|
|
162
|
+
"""
|
|
163
|
+
Record a value in a histogram.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
name: The metric name.
|
|
167
|
+
value: The value to record.
|
|
168
|
+
tags: Optional tags/labels for the metric.
|
|
169
|
+
"""
|
|
170
|
+
...
|
|
171
|
+
|
|
172
|
+
def timing(self, name: str, duration_ms: float, tags: dict[str, Any] | None = None) -> None:
|
|
173
|
+
"""
|
|
174
|
+
Record a timing/duration measurement.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
name: The metric name.
|
|
178
|
+
duration_ms: The duration in milliseconds.
|
|
179
|
+
tags: Optional tags/labels for the metric.
|
|
180
|
+
"""
|
|
181
|
+
...
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class LoggingMetricHook:
|
|
185
|
+
"""
|
|
186
|
+
Simple hook that logs metrics (for development and debugging).
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
>>> import logging
|
|
190
|
+
>>> logging.basicConfig(level=logging.DEBUG)
|
|
191
|
+
>>> hook = LoggingMetricHook()
|
|
192
|
+
>>> hook.increment("requests", 1.0, {"service": "api"})
|
|
193
|
+
DEBUG:proxilion.metrics:COUNTER requests=1.0 tags={'service': 'api'}
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def __init__(self, logger: logging.Logger | None = None, level: int = logging.DEBUG):
|
|
197
|
+
"""
|
|
198
|
+
Initialize the logging hook.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
logger: Logger instance to use. Defaults to "proxilion.metrics".
|
|
202
|
+
level: Logging level for metric messages.
|
|
203
|
+
"""
|
|
204
|
+
self.logger = logger or logging.getLogger("proxilion.metrics")
|
|
205
|
+
self.level = level
|
|
206
|
+
|
|
207
|
+
def increment(self, name: str, value: float = 1.0, tags: dict[str, Any] | None = None) -> None:
|
|
208
|
+
"""Log a counter increment."""
|
|
209
|
+
self.logger.log(self.level, f"COUNTER {name}={value} tags={tags}")
|
|
210
|
+
|
|
211
|
+
def gauge(self, name: str, value: float, tags: dict[str, Any] | None = None) -> None:
|
|
212
|
+
"""Log a gauge value."""
|
|
213
|
+
self.logger.log(self.level, f"GAUGE {name}={value} tags={tags}")
|
|
214
|
+
|
|
215
|
+
def histogram(self, name: str, value: float, tags: dict[str, Any] | None = None) -> None:
|
|
216
|
+
"""Log a histogram value."""
|
|
217
|
+
self.logger.log(self.level, f"HISTOGRAM {name}={value} tags={tags}")
|
|
218
|
+
|
|
219
|
+
def timing(self, name: str, duration_ms: float, tags: dict[str, Any] | None = None) -> None:
|
|
220
|
+
"""Log a timing value."""
|
|
221
|
+
self.logger.log(self.level, f"TIMING {name}={duration_ms}ms tags={tags}")
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass
|
|
225
|
+
class HistogramStats:
|
|
226
|
+
"""Statistics for a histogram metric."""
|
|
227
|
+
|
|
228
|
+
count: int
|
|
229
|
+
"""Number of recorded values."""
|
|
230
|
+
|
|
231
|
+
total: float
|
|
232
|
+
"""Sum of all values."""
|
|
233
|
+
|
|
234
|
+
min: float
|
|
235
|
+
"""Minimum value."""
|
|
236
|
+
|
|
237
|
+
max: float
|
|
238
|
+
"""Maximum value."""
|
|
239
|
+
|
|
240
|
+
avg: float
|
|
241
|
+
"""Average value."""
|
|
242
|
+
|
|
243
|
+
def to_dict(self) -> dict[str, Any]:
|
|
244
|
+
"""Convert to dictionary."""
|
|
245
|
+
return {
|
|
246
|
+
"count": self.count,
|
|
247
|
+
"total": self.total,
|
|
248
|
+
"min": self.min,
|
|
249
|
+
"max": self.max,
|
|
250
|
+
"avg": self.avg,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
class InMemoryMetricHook:
|
|
255
|
+
"""
|
|
256
|
+
In-memory metrics for testing and simple use cases.
|
|
257
|
+
|
|
258
|
+
Stores all metrics in memory and provides methods to query them.
|
|
259
|
+
Useful for testing that metrics are being emitted correctly.
|
|
260
|
+
|
|
261
|
+
Example:
|
|
262
|
+
>>> hook = InMemoryMetricHook()
|
|
263
|
+
>>> hook.increment("requests", tags={"user": "alice"})
|
|
264
|
+
>>> hook.increment("requests", tags={"user": "alice"})
|
|
265
|
+
>>> hook.increment("requests", tags={"user": "bob"})
|
|
266
|
+
>>>
|
|
267
|
+
>>> hook.get_counter("requests", {"user": "alice"})
|
|
268
|
+
2.0
|
|
269
|
+
>>> hook.get_counter("requests", {"user": "bob"})
|
|
270
|
+
1.0
|
|
271
|
+
>>>
|
|
272
|
+
>>> hook.histogram("latency", 100.0)
|
|
273
|
+
>>> hook.histogram("latency", 200.0)
|
|
274
|
+
>>> stats = hook.get_histogram_stats("latency")
|
|
275
|
+
>>> stats.avg
|
|
276
|
+
150.0
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def __init__(self):
|
|
280
|
+
"""Initialize the in-memory hook."""
|
|
281
|
+
self.counters: dict[str, float] = defaultdict(float)
|
|
282
|
+
self.gauges: dict[str, float] = {}
|
|
283
|
+
self.histograms: dict[str, list[float]] = defaultdict(list)
|
|
284
|
+
self.timings: dict[str, list[float]] = defaultdict(list)
|
|
285
|
+
|
|
286
|
+
def _make_key(self, name: str, tags: dict[str, Any] | None) -> str:
|
|
287
|
+
"""Create a unique key from metric name and tags."""
|
|
288
|
+
if not tags:
|
|
289
|
+
return name
|
|
290
|
+
sorted_tags = sorted(tags.items())
|
|
291
|
+
tag_str = ",".join(f"{k}={v}" for k, v in sorted_tags)
|
|
292
|
+
return f"{name}[{tag_str}]"
|
|
293
|
+
|
|
294
|
+
def increment(self, name: str, value: float = 1.0, tags: dict[str, Any] | None = None) -> None:
|
|
295
|
+
"""Increment a counter."""
|
|
296
|
+
key = self._make_key(name, tags)
|
|
297
|
+
self.counters[key] += value
|
|
298
|
+
|
|
299
|
+
def gauge(self, name: str, value: float, tags: dict[str, Any] | None = None) -> None:
|
|
300
|
+
"""Set a gauge value."""
|
|
301
|
+
key = self._make_key(name, tags)
|
|
302
|
+
self.gauges[key] = value
|
|
303
|
+
|
|
304
|
+
def histogram(self, name: str, value: float, tags: dict[str, Any] | None = None) -> None:
|
|
305
|
+
"""Record a histogram value."""
|
|
306
|
+
key = self._make_key(name, tags)
|
|
307
|
+
self.histograms[key].append(value)
|
|
308
|
+
|
|
309
|
+
def timing(self, name: str, duration_ms: float, tags: dict[str, Any] | None = None) -> None:
|
|
310
|
+
"""Record a timing value."""
|
|
311
|
+
key = self._make_key(name, tags)
|
|
312
|
+
self.timings[key].append(duration_ms)
|
|
313
|
+
|
|
314
|
+
def get_counter(self, name: str, tags: dict[str, Any] | None = None) -> float:
|
|
315
|
+
"""
|
|
316
|
+
Get the current value of a counter.
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
name: The metric name.
|
|
320
|
+
tags: Optional tags/labels for the metric.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
The current counter value, or 0.0 if not found.
|
|
324
|
+
"""
|
|
325
|
+
key = self._make_key(name, tags)
|
|
326
|
+
return self.counters[key]
|
|
327
|
+
|
|
328
|
+
def get_gauge(self, name: str, tags: dict[str, Any] | None = None) -> float | None:
|
|
329
|
+
"""
|
|
330
|
+
Get the current value of a gauge.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
name: The metric name.
|
|
334
|
+
tags: Optional tags/labels for the metric.
|
|
335
|
+
|
|
336
|
+
Returns:
|
|
337
|
+
The current gauge value, or None if not set.
|
|
338
|
+
"""
|
|
339
|
+
key = self._make_key(name, tags)
|
|
340
|
+
return self.gauges.get(key)
|
|
341
|
+
|
|
342
|
+
def get_histogram_stats(
|
|
343
|
+
self, name: str, tags: dict[str, Any] | None = None
|
|
344
|
+
) -> HistogramStats | None:
|
|
345
|
+
"""
|
|
346
|
+
Get statistics for a histogram.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
name: The metric name.
|
|
350
|
+
tags: Optional tags/labels for the metric.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
HistogramStats with count, sum, min, max, avg, or None if empty.
|
|
354
|
+
"""
|
|
355
|
+
key = self._make_key(name, tags)
|
|
356
|
+
values = self.histograms.get(key, [])
|
|
357
|
+
if not values:
|
|
358
|
+
return None
|
|
359
|
+
return HistogramStats(
|
|
360
|
+
count=len(values),
|
|
361
|
+
total=sum(values),
|
|
362
|
+
min=min(values),
|
|
363
|
+
max=max(values),
|
|
364
|
+
avg=sum(values) / len(values),
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
def get_timing_stats(
|
|
368
|
+
self, name: str, tags: dict[str, Any] | None = None
|
|
369
|
+
) -> HistogramStats | None:
|
|
370
|
+
"""
|
|
371
|
+
Get statistics for timing measurements.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
name: The metric name.
|
|
375
|
+
tags: Optional tags/labels for the metric.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
HistogramStats with count, sum, min, max, avg, or None if empty.
|
|
379
|
+
"""
|
|
380
|
+
key = self._make_key(name, tags)
|
|
381
|
+
values = self.timings.get(key, [])
|
|
382
|
+
if not values:
|
|
383
|
+
return None
|
|
384
|
+
return HistogramStats(
|
|
385
|
+
count=len(values),
|
|
386
|
+
total=sum(values),
|
|
387
|
+
min=min(values),
|
|
388
|
+
max=max(values),
|
|
389
|
+
avg=sum(values) / len(values),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
def get_all_counters(self) -> dict[str, float]:
|
|
393
|
+
"""Get all counter values."""
|
|
394
|
+
return dict(self.counters)
|
|
395
|
+
|
|
396
|
+
def get_all_gauges(self) -> dict[str, float]:
|
|
397
|
+
"""Get all gauge values."""
|
|
398
|
+
return dict(self.gauges)
|
|
399
|
+
|
|
400
|
+
def reset(self) -> None:
|
|
401
|
+
"""Reset all metrics to initial state."""
|
|
402
|
+
self.counters.clear()
|
|
403
|
+
self.gauges.clear()
|
|
404
|
+
self.histograms.clear()
|
|
405
|
+
self.timings.clear()
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
class ObservabilityHooks:
|
|
409
|
+
"""
|
|
410
|
+
Central registry for observability hooks.
|
|
411
|
+
|
|
412
|
+
This is a singleton that manages all metric hooks. Use `get_instance()`
|
|
413
|
+
to get the global instance.
|
|
414
|
+
|
|
415
|
+
Example:
|
|
416
|
+
>>> from proxilion.observability import ObservabilityHooks, InMemoryMetricHook
|
|
417
|
+
>>>
|
|
418
|
+
>>> hooks = ObservabilityHooks.get_instance()
|
|
419
|
+
>>> memory_hook = InMemoryMetricHook()
|
|
420
|
+
>>> hooks.add_metric_hook(memory_hook)
|
|
421
|
+
>>>
|
|
422
|
+
>>> # Emit metrics (these will be sent to all registered hooks)
|
|
423
|
+
>>> hooks.emit_counter("requests", tags={"service": "api"})
|
|
424
|
+
>>> hooks.emit_timing("latency", 45.2)
|
|
425
|
+
>>>
|
|
426
|
+
>>> # Check the recorded metrics
|
|
427
|
+
>>> memory_hook.get_counter("requests", {"service": "api"})
|
|
428
|
+
1.0
|
|
429
|
+
"""
|
|
430
|
+
|
|
431
|
+
_instance: ObservabilityHooks | None = None
|
|
432
|
+
|
|
433
|
+
def __init__(self):
|
|
434
|
+
"""Initialize the observability hooks registry."""
|
|
435
|
+
self._metric_hooks: list[MetricHook] = []
|
|
436
|
+
|
|
437
|
+
@classmethod
|
|
438
|
+
def get_instance(cls) -> ObservabilityHooks:
|
|
439
|
+
"""
|
|
440
|
+
Get the global ObservabilityHooks instance.
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
The singleton instance.
|
|
444
|
+
"""
|
|
445
|
+
if cls._instance is None:
|
|
446
|
+
cls._instance = cls()
|
|
447
|
+
return cls._instance
|
|
448
|
+
|
|
449
|
+
@classmethod
|
|
450
|
+
def reset_instance(cls) -> None:
|
|
451
|
+
"""
|
|
452
|
+
Reset the global instance.
|
|
453
|
+
|
|
454
|
+
Useful for testing to ensure a clean state.
|
|
455
|
+
"""
|
|
456
|
+
cls._instance = None
|
|
457
|
+
|
|
458
|
+
def add_metric_hook(self, hook: MetricHook) -> None:
|
|
459
|
+
"""
|
|
460
|
+
Register a metric hook.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
hook: The metric hook to register.
|
|
464
|
+
"""
|
|
465
|
+
self._metric_hooks.append(hook)
|
|
466
|
+
|
|
467
|
+
def remove_metric_hook(self, hook: MetricHook) -> bool:
|
|
468
|
+
"""
|
|
469
|
+
Remove a metric hook.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
hook: The metric hook to remove.
|
|
473
|
+
|
|
474
|
+
Returns:
|
|
475
|
+
True if the hook was removed, False if not found.
|
|
476
|
+
"""
|
|
477
|
+
try:
|
|
478
|
+
self._metric_hooks.remove(hook)
|
|
479
|
+
return True
|
|
480
|
+
except ValueError:
|
|
481
|
+
return False
|
|
482
|
+
|
|
483
|
+
def clear_metric_hooks(self) -> None:
|
|
484
|
+
"""Remove all registered metric hooks."""
|
|
485
|
+
self._metric_hooks.clear()
|
|
486
|
+
|
|
487
|
+
@property
|
|
488
|
+
def metric_hooks(self) -> list[MetricHook]:
|
|
489
|
+
"""Get a copy of the registered metric hooks."""
|
|
490
|
+
return list(self._metric_hooks)
|
|
491
|
+
|
|
492
|
+
def emit_metric(
|
|
493
|
+
self,
|
|
494
|
+
metric_type: MetricType,
|
|
495
|
+
name: str,
|
|
496
|
+
value: float,
|
|
497
|
+
tags: dict[str, Any] | None = None,
|
|
498
|
+
) -> None:
|
|
499
|
+
"""
|
|
500
|
+
Emit a metric to all registered hooks.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
metric_type: The type of metric.
|
|
504
|
+
name: The metric name.
|
|
505
|
+
value: The metric value.
|
|
506
|
+
tags: Optional tags/labels for the metric.
|
|
507
|
+
"""
|
|
508
|
+
for hook in self._metric_hooks:
|
|
509
|
+
if metric_type == MetricType.COUNTER:
|
|
510
|
+
hook.increment(name, value, tags)
|
|
511
|
+
elif metric_type == MetricType.GAUGE:
|
|
512
|
+
hook.gauge(name, value, tags)
|
|
513
|
+
elif metric_type == MetricType.HISTOGRAM:
|
|
514
|
+
hook.histogram(name, value, tags)
|
|
515
|
+
elif metric_type in (MetricType.TIMING, MetricType.SUMMARY):
|
|
516
|
+
hook.timing(name, value, tags)
|
|
517
|
+
|
|
518
|
+
def emit_counter(
|
|
519
|
+
self, name: str, value: float = 1.0, tags: dict[str, Any] | None = None
|
|
520
|
+
) -> None:
|
|
521
|
+
"""
|
|
522
|
+
Emit a counter metric.
|
|
523
|
+
|
|
524
|
+
Args:
|
|
525
|
+
name: The metric name.
|
|
526
|
+
value: The amount to increment by (default 1.0).
|
|
527
|
+
tags: Optional tags/labels for the metric.
|
|
528
|
+
"""
|
|
529
|
+
self.emit_metric(MetricType.COUNTER, name, value, tags)
|
|
530
|
+
|
|
531
|
+
def emit_gauge(self, name: str, value: float, tags: dict[str, Any] | None = None) -> None:
|
|
532
|
+
"""
|
|
533
|
+
Emit a gauge metric.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
name: The metric name.
|
|
537
|
+
value: The gauge value.
|
|
538
|
+
tags: Optional tags/labels for the metric.
|
|
539
|
+
"""
|
|
540
|
+
self.emit_metric(MetricType.GAUGE, name, value, tags)
|
|
541
|
+
|
|
542
|
+
def emit_histogram(self, name: str, value: float, tags: dict[str, Any] | None = None) -> None:
|
|
543
|
+
"""
|
|
544
|
+
Emit a histogram metric.
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
name: The metric name.
|
|
548
|
+
value: The value to record.
|
|
549
|
+
tags: Optional tags/labels for the metric.
|
|
550
|
+
"""
|
|
551
|
+
self.emit_metric(MetricType.HISTOGRAM, name, value, tags)
|
|
552
|
+
|
|
553
|
+
def emit_timing(
|
|
554
|
+
self, name: str, duration_ms: float, tags: dict[str, Any] | None = None
|
|
555
|
+
) -> None:
|
|
556
|
+
"""
|
|
557
|
+
Emit a timing metric.
|
|
558
|
+
|
|
559
|
+
Args:
|
|
560
|
+
name: The metric name.
|
|
561
|
+
duration_ms: The duration in milliseconds.
|
|
562
|
+
tags: Optional tags/labels for the metric.
|
|
563
|
+
"""
|
|
564
|
+
self.emit_metric(MetricType.TIMING, name, duration_ms, tags)
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
# Standard metric names emitted by Proxilion
|
|
568
|
+
# These are the metric names that Proxilion components emit
|
|
569
|
+
|
|
570
|
+
# Authorization metrics
|
|
571
|
+
METRIC_AUTH_REQUESTS = "proxilion.auth.requests"
|
|
572
|
+
"""Counter: Total authorization requests."""
|
|
573
|
+
|
|
574
|
+
METRIC_AUTH_ALLOWED = "proxilion.auth.allowed"
|
|
575
|
+
"""Counter: Authorization requests that were allowed."""
|
|
576
|
+
|
|
577
|
+
METRIC_AUTH_DENIED = "proxilion.auth.denied"
|
|
578
|
+
"""Counter: Authorization requests that were denied."""
|
|
579
|
+
|
|
580
|
+
METRIC_AUTH_LATENCY = "proxilion.auth.latency_ms"
|
|
581
|
+
"""Histogram: Authorization check latency in milliseconds."""
|
|
582
|
+
|
|
583
|
+
# Rate limiting metrics
|
|
584
|
+
METRIC_RATE_LIMIT_REQUESTS = "proxilion.rate_limit.requests"
|
|
585
|
+
"""Counter: Total rate limit checks."""
|
|
586
|
+
|
|
587
|
+
METRIC_RATE_LIMIT_EXCEEDED = "proxilion.rate_limit.exceeded"
|
|
588
|
+
"""Counter: Rate limit exceeded events."""
|
|
589
|
+
|
|
590
|
+
# Tool execution metrics
|
|
591
|
+
METRIC_TOOL_CALLS = "proxilion.tool.calls"
|
|
592
|
+
"""Counter: Total tool call executions."""
|
|
593
|
+
|
|
594
|
+
METRIC_TOOL_LATENCY = "proxilion.tool.latency_ms"
|
|
595
|
+
"""Histogram: Tool execution latency in milliseconds."""
|
|
596
|
+
|
|
597
|
+
METRIC_TOOL_ERRORS = "proxilion.tool.errors"
|
|
598
|
+
"""Counter: Tool execution errors."""
|
|
599
|
+
|
|
600
|
+
# Cost metrics
|
|
601
|
+
METRIC_COST_USD = "proxilion.cost.usd"
|
|
602
|
+
"""Counter: Total cost in USD."""
|
|
603
|
+
|
|
604
|
+
METRIC_TOKENS_INPUT = "proxilion.tokens.input"
|
|
605
|
+
"""Counter: Total input tokens processed."""
|
|
606
|
+
|
|
607
|
+
METRIC_TOKENS_OUTPUT = "proxilion.tokens.output"
|
|
608
|
+
"""Counter: Total output tokens generated."""
|
|
609
|
+
|
|
610
|
+
# Circuit breaker metrics
|
|
611
|
+
METRIC_CIRCUIT_BREAKER_OPEN = "proxilion.circuit_breaker.open"
|
|
612
|
+
"""Counter: Circuit breaker open events."""
|
|
613
|
+
|
|
614
|
+
METRIC_CIRCUIT_BREAKER_HALF_OPEN = "proxilion.circuit_breaker.half_open"
|
|
615
|
+
"""Counter: Circuit breaker half-open events."""
|
|
616
|
+
|
|
617
|
+
METRIC_CIRCUIT_BREAKER_CLOSED = "proxilion.circuit_breaker.closed"
|
|
618
|
+
"""Counter: Circuit breaker closed events."""
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
# Convenience functions for emitting metrics
|
|
622
|
+
def emit_counter(name: str, value: float = 1.0, tags: dict[str, Any] | None = None) -> None:
|
|
623
|
+
"""
|
|
624
|
+
Emit a counter metric to all registered hooks.
|
|
625
|
+
|
|
626
|
+
Args:
|
|
627
|
+
name: The metric name (e.g., "proxilion.auth.requests").
|
|
628
|
+
value: The amount to increment by (default 1.0).
|
|
629
|
+
tags: Optional tags/labels for the metric.
|
|
630
|
+
|
|
631
|
+
Example:
|
|
632
|
+
>>> from proxilion.observability import emit_counter
|
|
633
|
+
>>> emit_counter("proxilion.auth.requests", tags={"user": "alice"})
|
|
634
|
+
"""
|
|
635
|
+
ObservabilityHooks.get_instance().emit_counter(name, value, tags)
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
def emit_gauge(name: str, value: float, tags: dict[str, Any] | None = None) -> None:
|
|
639
|
+
"""
|
|
640
|
+
Emit a gauge metric to all registered hooks.
|
|
641
|
+
|
|
642
|
+
Args:
|
|
643
|
+
name: The metric name.
|
|
644
|
+
value: The gauge value.
|
|
645
|
+
tags: Optional tags/labels for the metric.
|
|
646
|
+
|
|
647
|
+
Example:
|
|
648
|
+
>>> from proxilion.observability import emit_gauge
|
|
649
|
+
>>> emit_gauge("proxilion.connections.active", 42)
|
|
650
|
+
"""
|
|
651
|
+
ObservabilityHooks.get_instance().emit_gauge(name, value, tags)
|
|
652
|
+
|
|
653
|
+
|
|
654
|
+
def emit_histogram(name: str, value: float, tags: dict[str, Any] | None = None) -> None:
|
|
655
|
+
"""
|
|
656
|
+
Emit a histogram metric to all registered hooks.
|
|
657
|
+
|
|
658
|
+
Args:
|
|
659
|
+
name: The metric name.
|
|
660
|
+
value: The value to record.
|
|
661
|
+
tags: Optional tags/labels for the metric.
|
|
662
|
+
|
|
663
|
+
Example:
|
|
664
|
+
>>> from proxilion.observability import emit_histogram
|
|
665
|
+
>>> emit_histogram("proxilion.response.size_bytes", 1024)
|
|
666
|
+
"""
|
|
667
|
+
ObservabilityHooks.get_instance().emit_histogram(name, value, tags)
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
def emit_timing(name: str, duration_ms: float, tags: dict[str, Any] | None = None) -> None:
|
|
671
|
+
"""
|
|
672
|
+
Emit a timing metric to all registered hooks.
|
|
673
|
+
|
|
674
|
+
Args:
|
|
675
|
+
name: The metric name.
|
|
676
|
+
duration_ms: The duration in milliseconds.
|
|
677
|
+
tags: Optional tags/labels for the metric.
|
|
678
|
+
|
|
679
|
+
Example:
|
|
680
|
+
>>> from proxilion.observability import emit_timing
|
|
681
|
+
>>> emit_timing("proxilion.auth.latency_ms", 45.2, tags={"tool": "search"})
|
|
682
|
+
"""
|
|
683
|
+
ObservabilityHooks.get_instance().emit_timing(name, duration_ms, tags)
|