ccproxy-api 0.1.0__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.
- ccproxy/__init__.py +4 -0
- ccproxy/__main__.py +7 -0
- ccproxy/_version.py +21 -0
- ccproxy/adapters/__init__.py +11 -0
- ccproxy/adapters/base.py +80 -0
- ccproxy/adapters/openai/__init__.py +43 -0
- ccproxy/adapters/openai/adapter.py +915 -0
- ccproxy/adapters/openai/models.py +412 -0
- ccproxy/adapters/openai/streaming.py +449 -0
- ccproxy/api/__init__.py +28 -0
- ccproxy/api/app.py +225 -0
- ccproxy/api/dependencies.py +140 -0
- ccproxy/api/middleware/__init__.py +11 -0
- ccproxy/api/middleware/auth.py +0 -0
- ccproxy/api/middleware/cors.py +55 -0
- ccproxy/api/middleware/errors.py +703 -0
- ccproxy/api/middleware/headers.py +51 -0
- ccproxy/api/middleware/logging.py +175 -0
- ccproxy/api/middleware/request_id.py +69 -0
- ccproxy/api/middleware/server_header.py +62 -0
- ccproxy/api/responses.py +84 -0
- ccproxy/api/routes/__init__.py +16 -0
- ccproxy/api/routes/claude.py +181 -0
- ccproxy/api/routes/health.py +489 -0
- ccproxy/api/routes/metrics.py +1033 -0
- ccproxy/api/routes/proxy.py +238 -0
- ccproxy/auth/__init__.py +75 -0
- ccproxy/auth/bearer.py +68 -0
- ccproxy/auth/credentials_adapter.py +93 -0
- ccproxy/auth/dependencies.py +229 -0
- ccproxy/auth/exceptions.py +79 -0
- ccproxy/auth/manager.py +102 -0
- ccproxy/auth/models.py +118 -0
- ccproxy/auth/oauth/__init__.py +26 -0
- ccproxy/auth/oauth/models.py +49 -0
- ccproxy/auth/oauth/routes.py +396 -0
- ccproxy/auth/oauth/storage.py +0 -0
- ccproxy/auth/storage/__init__.py +12 -0
- ccproxy/auth/storage/base.py +57 -0
- ccproxy/auth/storage/json_file.py +159 -0
- ccproxy/auth/storage/keyring.py +192 -0
- ccproxy/claude_sdk/__init__.py +20 -0
- ccproxy/claude_sdk/client.py +169 -0
- ccproxy/claude_sdk/converter.py +331 -0
- ccproxy/claude_sdk/options.py +120 -0
- ccproxy/cli/__init__.py +14 -0
- ccproxy/cli/commands/__init__.py +8 -0
- ccproxy/cli/commands/auth.py +553 -0
- ccproxy/cli/commands/config/__init__.py +14 -0
- ccproxy/cli/commands/config/commands.py +766 -0
- ccproxy/cli/commands/config/schema_commands.py +119 -0
- ccproxy/cli/commands/serve.py +630 -0
- ccproxy/cli/docker/__init__.py +34 -0
- ccproxy/cli/docker/adapter_factory.py +157 -0
- ccproxy/cli/docker/params.py +278 -0
- ccproxy/cli/helpers.py +144 -0
- ccproxy/cli/main.py +193 -0
- ccproxy/cli/options/__init__.py +14 -0
- ccproxy/cli/options/claude_options.py +216 -0
- ccproxy/cli/options/core_options.py +40 -0
- ccproxy/cli/options/security_options.py +48 -0
- ccproxy/cli/options/server_options.py +117 -0
- ccproxy/config/__init__.py +40 -0
- ccproxy/config/auth.py +154 -0
- ccproxy/config/claude.py +124 -0
- ccproxy/config/cors.py +79 -0
- ccproxy/config/discovery.py +87 -0
- ccproxy/config/docker_settings.py +265 -0
- ccproxy/config/loader.py +108 -0
- ccproxy/config/observability.py +158 -0
- ccproxy/config/pricing.py +88 -0
- ccproxy/config/reverse_proxy.py +31 -0
- ccproxy/config/scheduler.py +89 -0
- ccproxy/config/security.py +14 -0
- ccproxy/config/server.py +81 -0
- ccproxy/config/settings.py +534 -0
- ccproxy/config/validators.py +231 -0
- ccproxy/core/__init__.py +274 -0
- ccproxy/core/async_utils.py +675 -0
- ccproxy/core/constants.py +97 -0
- ccproxy/core/errors.py +256 -0
- ccproxy/core/http.py +328 -0
- ccproxy/core/http_transformers.py +428 -0
- ccproxy/core/interfaces.py +247 -0
- ccproxy/core/logging.py +189 -0
- ccproxy/core/middleware.py +114 -0
- ccproxy/core/proxy.py +143 -0
- ccproxy/core/system.py +38 -0
- ccproxy/core/transformers.py +259 -0
- ccproxy/core/types.py +129 -0
- ccproxy/core/validators.py +288 -0
- ccproxy/docker/__init__.py +67 -0
- ccproxy/docker/adapter.py +588 -0
- ccproxy/docker/docker_path.py +207 -0
- ccproxy/docker/middleware.py +103 -0
- ccproxy/docker/models.py +228 -0
- ccproxy/docker/protocol.py +192 -0
- ccproxy/docker/stream_process.py +264 -0
- ccproxy/docker/validators.py +173 -0
- ccproxy/models/__init__.py +123 -0
- ccproxy/models/errors.py +42 -0
- ccproxy/models/messages.py +243 -0
- ccproxy/models/requests.py +85 -0
- ccproxy/models/responses.py +227 -0
- ccproxy/models/types.py +102 -0
- ccproxy/observability/__init__.py +51 -0
- ccproxy/observability/access_logger.py +400 -0
- ccproxy/observability/context.py +447 -0
- ccproxy/observability/metrics.py +539 -0
- ccproxy/observability/pushgateway.py +366 -0
- ccproxy/observability/sse_events.py +303 -0
- ccproxy/observability/stats_printer.py +755 -0
- ccproxy/observability/storage/__init__.py +1 -0
- ccproxy/observability/storage/duckdb_simple.py +665 -0
- ccproxy/observability/storage/models.py +55 -0
- ccproxy/pricing/__init__.py +19 -0
- ccproxy/pricing/cache.py +212 -0
- ccproxy/pricing/loader.py +267 -0
- ccproxy/pricing/models.py +106 -0
- ccproxy/pricing/updater.py +309 -0
- ccproxy/scheduler/__init__.py +39 -0
- ccproxy/scheduler/core.py +335 -0
- ccproxy/scheduler/exceptions.py +34 -0
- ccproxy/scheduler/manager.py +186 -0
- ccproxy/scheduler/registry.py +150 -0
- ccproxy/scheduler/tasks.py +484 -0
- ccproxy/services/__init__.py +10 -0
- ccproxy/services/claude_sdk_service.py +614 -0
- ccproxy/services/credentials/__init__.py +55 -0
- ccproxy/services/credentials/config.py +105 -0
- ccproxy/services/credentials/manager.py +562 -0
- ccproxy/services/credentials/oauth_client.py +482 -0
- ccproxy/services/proxy_service.py +1536 -0
- ccproxy/static/.keep +0 -0
- ccproxy/testing/__init__.py +34 -0
- ccproxy/testing/config.py +148 -0
- ccproxy/testing/content_generation.py +197 -0
- ccproxy/testing/mock_responses.py +262 -0
- ccproxy/testing/response_handlers.py +161 -0
- ccproxy/testing/scenarios.py +241 -0
- ccproxy/utils/__init__.py +6 -0
- ccproxy/utils/cost_calculator.py +210 -0
- ccproxy/utils/streaming_metrics.py +199 -0
- ccproxy_api-0.1.0.dist-info/METADATA +253 -0
- ccproxy_api-0.1.0.dist-info/RECORD +148 -0
- ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
- ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
- ccproxy_api-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,539 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Prometheus metrics for operational monitoring.
|
|
3
|
+
|
|
4
|
+
This module provides direct prometheus_client integration for fast operational metrics
|
|
5
|
+
like request counts, response times, and resource usage. These metrics are optimized
|
|
6
|
+
for real-time monitoring and alerting.
|
|
7
|
+
|
|
8
|
+
Key features:
|
|
9
|
+
- Thread-safe metric operations using prometheus_client
|
|
10
|
+
- Minimal overhead for high-frequency operations
|
|
11
|
+
- Standard Prometheus metric types (Counter, Histogram, Gauge)
|
|
12
|
+
- Automatic label management and validation
|
|
13
|
+
- Pushgateway integration for batch metric pushing
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import logging
|
|
20
|
+
from typing import Any, Optional, Union
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from prometheus_client import CollectorRegistry, Counter, Gauge, Histogram, Info
|
|
25
|
+
|
|
26
|
+
PROMETHEUS_AVAILABLE = True
|
|
27
|
+
except ImportError:
|
|
28
|
+
PROMETHEUS_AVAILABLE = False
|
|
29
|
+
|
|
30
|
+
# Create dummy classes for graceful degradation
|
|
31
|
+
class _DummyCounter:
|
|
32
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def labels(self, **kwargs: Any) -> _DummyCounter:
|
|
36
|
+
return self
|
|
37
|
+
|
|
38
|
+
def inc(self, value: float = 1) -> None:
|
|
39
|
+
pass
|
|
40
|
+
|
|
41
|
+
class _DummyHistogram:
|
|
42
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
def labels(self, **kwargs: Any) -> _DummyHistogram:
|
|
46
|
+
return self
|
|
47
|
+
|
|
48
|
+
def observe(self, value: float) -> None:
|
|
49
|
+
pass
|
|
50
|
+
|
|
51
|
+
def time(self) -> _DummyHistogram:
|
|
52
|
+
return self
|
|
53
|
+
|
|
54
|
+
class _DummyGauge:
|
|
55
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
def labels(self, **kwargs: Any) -> _DummyGauge:
|
|
59
|
+
return self
|
|
60
|
+
|
|
61
|
+
def set(self, value: float) -> None:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
def inc(self, value: float = 1) -> None:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
def dec(self, value: float = 1) -> None:
|
|
68
|
+
pass
|
|
69
|
+
|
|
70
|
+
class _DummyInfo:
|
|
71
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
72
|
+
pass
|
|
73
|
+
|
|
74
|
+
def info(self, labels: dict[str, str]) -> None:
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
class _DummyCollectorRegistry:
|
|
78
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
# Assign dummy classes to the expected names
|
|
82
|
+
Counter = _DummyCounter # type: ignore[misc,assignment]
|
|
83
|
+
Histogram = _DummyHistogram # type: ignore[misc,assignment]
|
|
84
|
+
Gauge = _DummyGauge # type: ignore[misc,assignment]
|
|
85
|
+
Info = _DummyInfo # type: ignore[misc,assignment]
|
|
86
|
+
CollectorRegistry = _DummyCollectorRegistry # type: ignore[misc,assignment]
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
from structlog import get_logger
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
logger = get_logger(__name__)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class PrometheusMetrics:
|
|
96
|
+
"""
|
|
97
|
+
Prometheus metrics collector for operational monitoring.
|
|
98
|
+
|
|
99
|
+
Provides thread-safe, high-performance metrics collection using prometheus_client.
|
|
100
|
+
Designed for minimal overhead in request processing hot paths.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
def __init__(
|
|
104
|
+
self,
|
|
105
|
+
namespace: str = "ccproxy",
|
|
106
|
+
registry: CollectorRegistry | None = None,
|
|
107
|
+
pushgateway_client: Any | None = None,
|
|
108
|
+
):
|
|
109
|
+
"""
|
|
110
|
+
Initialize Prometheus metrics.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
namespace: Metric name prefix
|
|
114
|
+
registry: Custom Prometheus registry (uses default if None)
|
|
115
|
+
pushgateway_client: Optional pushgateway client for dependency injection
|
|
116
|
+
"""
|
|
117
|
+
if not PROMETHEUS_AVAILABLE:
|
|
118
|
+
logger.warning(
|
|
119
|
+
"prometheus_client not available. Metrics will be disabled. "
|
|
120
|
+
"Install with: pip install prometheus-client"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
self.namespace = namespace
|
|
124
|
+
# Use default registry if None is passed
|
|
125
|
+
if registry is None and PROMETHEUS_AVAILABLE:
|
|
126
|
+
from prometheus_client import REGISTRY
|
|
127
|
+
|
|
128
|
+
self.registry: CollectorRegistry | None = REGISTRY
|
|
129
|
+
else:
|
|
130
|
+
self.registry = registry
|
|
131
|
+
self._enabled = PROMETHEUS_AVAILABLE
|
|
132
|
+
self._pushgateway_client = pushgateway_client
|
|
133
|
+
|
|
134
|
+
if self._enabled:
|
|
135
|
+
self._init_metrics()
|
|
136
|
+
# Initialize pushgateway client if not provided via DI
|
|
137
|
+
if self._pushgateway_client is None:
|
|
138
|
+
self._init_pushgateway()
|
|
139
|
+
|
|
140
|
+
def _init_metrics(self) -> None:
|
|
141
|
+
"""Initialize all Prometheus metric objects."""
|
|
142
|
+
# Request metrics
|
|
143
|
+
self.request_counter = Counter(
|
|
144
|
+
f"{self.namespace}_requests_total",
|
|
145
|
+
"Total number of requests processed",
|
|
146
|
+
labelnames=["method", "endpoint", "model", "status", "service_type"],
|
|
147
|
+
registry=self.registry,
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
self.response_time = Histogram(
|
|
151
|
+
f"{self.namespace}_response_duration_seconds",
|
|
152
|
+
"Response time in seconds",
|
|
153
|
+
labelnames=["model", "endpoint", "service_type"],
|
|
154
|
+
buckets=[0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 25.0],
|
|
155
|
+
registry=self.registry,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Token metrics
|
|
159
|
+
self.token_counter = Counter(
|
|
160
|
+
f"{self.namespace}_tokens_total",
|
|
161
|
+
"Total tokens processed",
|
|
162
|
+
labelnames=[
|
|
163
|
+
"type",
|
|
164
|
+
"model",
|
|
165
|
+
"service_type",
|
|
166
|
+
], # _type: input, output, cache_read, cache_write
|
|
167
|
+
registry=self.registry,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
# Cost metrics
|
|
171
|
+
self.cost_counter = Counter(
|
|
172
|
+
f"{self.namespace}_cost_usd_total",
|
|
173
|
+
"Total cost in USD",
|
|
174
|
+
labelnames=[
|
|
175
|
+
"model",
|
|
176
|
+
"cost_type",
|
|
177
|
+
"service_type",
|
|
178
|
+
], # cost_type: input, output, cache, total
|
|
179
|
+
registry=self.registry,
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
# Error metrics
|
|
183
|
+
self.error_counter = Counter(
|
|
184
|
+
f"{self.namespace}_errors_total",
|
|
185
|
+
"Total number of errors",
|
|
186
|
+
labelnames=["error_type", "endpoint", "model", "service_type"],
|
|
187
|
+
registry=self.registry,
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Active requests gauge
|
|
191
|
+
self.active_requests = Gauge(
|
|
192
|
+
f"{self.namespace}_active_requests",
|
|
193
|
+
"Number of currently active requests",
|
|
194
|
+
registry=self.registry,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# System info
|
|
198
|
+
self.system_info = Info(
|
|
199
|
+
f"{self.namespace}_info", "System information", registry=self.registry
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Service up metric (for Grafana service health)
|
|
203
|
+
self.up = Gauge(
|
|
204
|
+
"up",
|
|
205
|
+
"Service is up and running",
|
|
206
|
+
labelnames=["job"],
|
|
207
|
+
registry=self.registry,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# Set initial system info
|
|
211
|
+
try:
|
|
212
|
+
from ccproxy import __version__
|
|
213
|
+
|
|
214
|
+
version = __version__
|
|
215
|
+
except ImportError:
|
|
216
|
+
version = "unknown"
|
|
217
|
+
|
|
218
|
+
self.system_info.info(
|
|
219
|
+
{
|
|
220
|
+
"version": version,
|
|
221
|
+
"metrics_enabled": "true",
|
|
222
|
+
}
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Set service as up
|
|
226
|
+
self.up.labels(job="ccproxy").set(1)
|
|
227
|
+
|
|
228
|
+
def _init_pushgateway(self) -> None:
|
|
229
|
+
"""Initialize Pushgateway client if configured (fallback for non-DI usage)."""
|
|
230
|
+
try:
|
|
231
|
+
# Import here to avoid circular imports
|
|
232
|
+
from ccproxy.config.settings import get_settings
|
|
233
|
+
|
|
234
|
+
from .pushgateway import PushgatewayClient
|
|
235
|
+
|
|
236
|
+
settings = get_settings()
|
|
237
|
+
|
|
238
|
+
self._pushgateway_client = PushgatewayClient(settings.observability)
|
|
239
|
+
|
|
240
|
+
if self._pushgateway_client.is_enabled():
|
|
241
|
+
logger.info(
|
|
242
|
+
"pushgateway_initialized: url=%s job=%s",
|
|
243
|
+
settings.observability.pushgateway_url,
|
|
244
|
+
settings.observability.pushgateway_job,
|
|
245
|
+
)
|
|
246
|
+
except Exception as e:
|
|
247
|
+
logger.warning("pushgateway_init_failed: error=%s", str(e))
|
|
248
|
+
self._pushgateway_client = None
|
|
249
|
+
|
|
250
|
+
def record_request(
|
|
251
|
+
self,
|
|
252
|
+
method: str,
|
|
253
|
+
endpoint: str,
|
|
254
|
+
model: str | None = None,
|
|
255
|
+
status: str | int = "unknown",
|
|
256
|
+
service_type: str | None = None,
|
|
257
|
+
) -> None:
|
|
258
|
+
"""
|
|
259
|
+
Record a request event.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
method: HTTP method (GET, POST, etc.)
|
|
263
|
+
endpoint: API endpoint path
|
|
264
|
+
model: Model name used
|
|
265
|
+
status: Response status code or status string
|
|
266
|
+
service_type: Service type (claude_sdk_service, proxy_service)
|
|
267
|
+
"""
|
|
268
|
+
if not self._enabled:
|
|
269
|
+
return
|
|
270
|
+
|
|
271
|
+
self.request_counter.labels(
|
|
272
|
+
method=method,
|
|
273
|
+
endpoint=endpoint,
|
|
274
|
+
model=model or "unknown",
|
|
275
|
+
status=str(status),
|
|
276
|
+
service_type=service_type or "unknown",
|
|
277
|
+
).inc()
|
|
278
|
+
|
|
279
|
+
def record_response_time(
|
|
280
|
+
self,
|
|
281
|
+
duration_seconds: float,
|
|
282
|
+
model: str | None = None,
|
|
283
|
+
endpoint: str = "unknown",
|
|
284
|
+
service_type: str | None = None,
|
|
285
|
+
) -> None:
|
|
286
|
+
"""
|
|
287
|
+
Record response time.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
duration_seconds: Response time in seconds
|
|
291
|
+
model: Model name used
|
|
292
|
+
endpoint: API endpoint
|
|
293
|
+
service_type: Service type (claude_sdk_service, proxy_service)
|
|
294
|
+
"""
|
|
295
|
+
if not self._enabled:
|
|
296
|
+
return
|
|
297
|
+
|
|
298
|
+
self.response_time.labels(
|
|
299
|
+
model=model or "unknown",
|
|
300
|
+
endpoint=endpoint,
|
|
301
|
+
service_type=service_type or "unknown",
|
|
302
|
+
).observe(duration_seconds)
|
|
303
|
+
|
|
304
|
+
def record_tokens(
|
|
305
|
+
self,
|
|
306
|
+
token_count: int,
|
|
307
|
+
token_type: str,
|
|
308
|
+
model: str | None = None,
|
|
309
|
+
service_type: str | None = None,
|
|
310
|
+
) -> None:
|
|
311
|
+
"""
|
|
312
|
+
Record token usage.
|
|
313
|
+
|
|
314
|
+
Args:
|
|
315
|
+
token_count: Number of tokens
|
|
316
|
+
token_type: Type of tokens (input, output, cache_read, cache_write)
|
|
317
|
+
model: Model name
|
|
318
|
+
service_type: Service type (claude_sdk_service, proxy_service)
|
|
319
|
+
"""
|
|
320
|
+
if not self._enabled or token_count <= 0:
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
self.token_counter.labels(
|
|
324
|
+
type=token_type,
|
|
325
|
+
model=model or "unknown",
|
|
326
|
+
service_type=service_type or "unknown",
|
|
327
|
+
).inc(token_count)
|
|
328
|
+
|
|
329
|
+
def record_cost(
|
|
330
|
+
self,
|
|
331
|
+
cost_usd: float,
|
|
332
|
+
model: str | None = None,
|
|
333
|
+
cost_type: str = "total",
|
|
334
|
+
service_type: str | None = None,
|
|
335
|
+
) -> None:
|
|
336
|
+
"""
|
|
337
|
+
Record cost.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
cost_usd: Cost in USD
|
|
341
|
+
model: Model name
|
|
342
|
+
cost_type: Type of cost (input, output, cache, total)
|
|
343
|
+
service_type: Service type (claude_sdk_service, proxy_service)
|
|
344
|
+
"""
|
|
345
|
+
if not self._enabled or cost_usd <= 0:
|
|
346
|
+
return
|
|
347
|
+
|
|
348
|
+
self.cost_counter.labels(
|
|
349
|
+
model=model or "unknown",
|
|
350
|
+
cost_type=cost_type,
|
|
351
|
+
service_type=service_type or "unknown",
|
|
352
|
+
).inc(cost_usd)
|
|
353
|
+
|
|
354
|
+
def record_error(
|
|
355
|
+
self,
|
|
356
|
+
error_type: str,
|
|
357
|
+
endpoint: str = "unknown",
|
|
358
|
+
model: str | None = None,
|
|
359
|
+
service_type: str | None = None,
|
|
360
|
+
) -> None:
|
|
361
|
+
"""
|
|
362
|
+
Record an error event.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
error_type: Type/name of error
|
|
366
|
+
endpoint: API endpoint where error occurred
|
|
367
|
+
model: Model name if applicable
|
|
368
|
+
service_type: Service type (claude_sdk_service, proxy_service)
|
|
369
|
+
"""
|
|
370
|
+
if not self._enabled:
|
|
371
|
+
return
|
|
372
|
+
|
|
373
|
+
self.error_counter.labels(
|
|
374
|
+
error_type=error_type,
|
|
375
|
+
endpoint=endpoint,
|
|
376
|
+
model=model or "unknown",
|
|
377
|
+
service_type=service_type or "unknown",
|
|
378
|
+
).inc()
|
|
379
|
+
|
|
380
|
+
def set_active_requests(self, count: int) -> None:
|
|
381
|
+
"""
|
|
382
|
+
Set the current number of active requests.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
count: Number of active requests
|
|
386
|
+
"""
|
|
387
|
+
if not self._enabled:
|
|
388
|
+
return
|
|
389
|
+
|
|
390
|
+
self.active_requests.set(count)
|
|
391
|
+
|
|
392
|
+
def inc_active_requests(self) -> None:
|
|
393
|
+
"""Increment active request counter."""
|
|
394
|
+
if not self._enabled:
|
|
395
|
+
return
|
|
396
|
+
|
|
397
|
+
self.active_requests.inc()
|
|
398
|
+
|
|
399
|
+
def dec_active_requests(self) -> None:
|
|
400
|
+
"""Decrement active request counter."""
|
|
401
|
+
if not self._enabled:
|
|
402
|
+
return
|
|
403
|
+
|
|
404
|
+
self.active_requests.dec()
|
|
405
|
+
|
|
406
|
+
def update_system_info(self, info: dict[str, str]) -> None:
|
|
407
|
+
"""
|
|
408
|
+
Update system information.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
info: Dictionary of system information key-value pairs
|
|
412
|
+
"""
|
|
413
|
+
if not self._enabled:
|
|
414
|
+
return
|
|
415
|
+
|
|
416
|
+
self.system_info.info(info)
|
|
417
|
+
|
|
418
|
+
def is_enabled(self) -> bool:
|
|
419
|
+
"""Check if metrics collection is enabled."""
|
|
420
|
+
return self._enabled
|
|
421
|
+
|
|
422
|
+
def push_to_gateway(self, method: str = "push") -> bool:
|
|
423
|
+
"""
|
|
424
|
+
Push current metrics to Pushgateway using official prometheus_client methods.
|
|
425
|
+
|
|
426
|
+
Args:
|
|
427
|
+
method: Push method - "push" (replace), "pushadd" (add), or "delete"
|
|
428
|
+
|
|
429
|
+
Returns:
|
|
430
|
+
True if push succeeded, False otherwise
|
|
431
|
+
"""
|
|
432
|
+
|
|
433
|
+
if not self._enabled or not self._pushgateway_client:
|
|
434
|
+
return False
|
|
435
|
+
|
|
436
|
+
result = self._pushgateway_client.push_metrics(self.registry, method)
|
|
437
|
+
return bool(result)
|
|
438
|
+
|
|
439
|
+
def push_add_to_gateway(self) -> bool:
|
|
440
|
+
"""
|
|
441
|
+
Add current metrics to existing job/instance in Pushgateway (pushadd operation).
|
|
442
|
+
|
|
443
|
+
This is useful when you want to add metrics without replacing existing ones.
|
|
444
|
+
|
|
445
|
+
Returns:
|
|
446
|
+
True if push succeeded, False otherwise
|
|
447
|
+
"""
|
|
448
|
+
return self.push_to_gateway(method="pushadd")
|
|
449
|
+
|
|
450
|
+
def delete_from_gateway(self) -> bool:
|
|
451
|
+
"""
|
|
452
|
+
Delete all metrics for the configured job from Pushgateway.
|
|
453
|
+
|
|
454
|
+
This removes all metrics associated with the job, useful for cleanup.
|
|
455
|
+
|
|
456
|
+
Returns:
|
|
457
|
+
True if delete succeeded, False otherwise
|
|
458
|
+
"""
|
|
459
|
+
|
|
460
|
+
if not self._enabled or not self._pushgateway_client:
|
|
461
|
+
return False
|
|
462
|
+
|
|
463
|
+
result = self._pushgateway_client.delete_metrics()
|
|
464
|
+
return bool(result)
|
|
465
|
+
|
|
466
|
+
def is_pushgateway_enabled(self) -> bool:
|
|
467
|
+
"""Check if Pushgateway client is enabled and configured."""
|
|
468
|
+
return (
|
|
469
|
+
self._pushgateway_client is not None
|
|
470
|
+
and self._pushgateway_client.is_enabled()
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# Global metrics instance
|
|
475
|
+
_global_metrics: PrometheusMetrics | None = None
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def get_metrics(
|
|
479
|
+
namespace: str = "ccproxy",
|
|
480
|
+
registry: CollectorRegistry | None = None,
|
|
481
|
+
pushgateway_client: Any | None = None,
|
|
482
|
+
settings: Any | None = None,
|
|
483
|
+
) -> PrometheusMetrics:
|
|
484
|
+
"""
|
|
485
|
+
Get or create global metrics instance with dependency injection.
|
|
486
|
+
|
|
487
|
+
Args:
|
|
488
|
+
namespace: Metric namespace prefix
|
|
489
|
+
registry: Custom Prometheus registry
|
|
490
|
+
pushgateway_client: Optional pushgateway client for dependency injection
|
|
491
|
+
settings: Optional settings instance to avoid circular imports
|
|
492
|
+
|
|
493
|
+
Returns:
|
|
494
|
+
PrometheusMetrics instance with full pushgateway support:
|
|
495
|
+
- push_to_gateway(): Replace all metrics (default)
|
|
496
|
+
- push_add_to_gateway(): Add metrics to existing job
|
|
497
|
+
- delete_from_gateway(): Delete all metrics for job
|
|
498
|
+
"""
|
|
499
|
+
global _global_metrics
|
|
500
|
+
|
|
501
|
+
if _global_metrics is None:
|
|
502
|
+
# Create pushgateway client if not provided via DI
|
|
503
|
+
if pushgateway_client is None:
|
|
504
|
+
from .pushgateway import get_pushgateway_client
|
|
505
|
+
|
|
506
|
+
pushgateway_client = get_pushgateway_client()
|
|
507
|
+
|
|
508
|
+
_global_metrics = PrometheusMetrics(
|
|
509
|
+
namespace=namespace,
|
|
510
|
+
registry=registry,
|
|
511
|
+
pushgateway_client=pushgateway_client,
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
return _global_metrics
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def reset_metrics() -> None:
|
|
518
|
+
"""Reset global metrics instance (mainly for testing)."""
|
|
519
|
+
global _global_metrics
|
|
520
|
+
_global_metrics = None
|
|
521
|
+
|
|
522
|
+
# Clear Prometheus registry to avoid duplicate metrics in tests
|
|
523
|
+
if PROMETHEUS_AVAILABLE:
|
|
524
|
+
try:
|
|
525
|
+
from prometheus_client import REGISTRY
|
|
526
|
+
|
|
527
|
+
# Clear all collectors from the registry
|
|
528
|
+
collectors = list(REGISTRY._collector_to_names.keys())
|
|
529
|
+
for collector in collectors:
|
|
530
|
+
REGISTRY.unregister(collector)
|
|
531
|
+
except Exception:
|
|
532
|
+
# If clearing the registry fails, just continue
|
|
533
|
+
# This is mainly for testing and shouldn't break functionality
|
|
534
|
+
pass
|
|
535
|
+
|
|
536
|
+
# Also reset pushgateway client
|
|
537
|
+
from .pushgateway import reset_pushgateway_client
|
|
538
|
+
|
|
539
|
+
reset_pushgateway_client()
|