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.
Files changed (148) hide show
  1. ccproxy/__init__.py +4 -0
  2. ccproxy/__main__.py +7 -0
  3. ccproxy/_version.py +21 -0
  4. ccproxy/adapters/__init__.py +11 -0
  5. ccproxy/adapters/base.py +80 -0
  6. ccproxy/adapters/openai/__init__.py +43 -0
  7. ccproxy/adapters/openai/adapter.py +915 -0
  8. ccproxy/adapters/openai/models.py +412 -0
  9. ccproxy/adapters/openai/streaming.py +449 -0
  10. ccproxy/api/__init__.py +28 -0
  11. ccproxy/api/app.py +225 -0
  12. ccproxy/api/dependencies.py +140 -0
  13. ccproxy/api/middleware/__init__.py +11 -0
  14. ccproxy/api/middleware/auth.py +0 -0
  15. ccproxy/api/middleware/cors.py +55 -0
  16. ccproxy/api/middleware/errors.py +703 -0
  17. ccproxy/api/middleware/headers.py +51 -0
  18. ccproxy/api/middleware/logging.py +175 -0
  19. ccproxy/api/middleware/request_id.py +69 -0
  20. ccproxy/api/middleware/server_header.py +62 -0
  21. ccproxy/api/responses.py +84 -0
  22. ccproxy/api/routes/__init__.py +16 -0
  23. ccproxy/api/routes/claude.py +181 -0
  24. ccproxy/api/routes/health.py +489 -0
  25. ccproxy/api/routes/metrics.py +1033 -0
  26. ccproxy/api/routes/proxy.py +238 -0
  27. ccproxy/auth/__init__.py +75 -0
  28. ccproxy/auth/bearer.py +68 -0
  29. ccproxy/auth/credentials_adapter.py +93 -0
  30. ccproxy/auth/dependencies.py +229 -0
  31. ccproxy/auth/exceptions.py +79 -0
  32. ccproxy/auth/manager.py +102 -0
  33. ccproxy/auth/models.py +118 -0
  34. ccproxy/auth/oauth/__init__.py +26 -0
  35. ccproxy/auth/oauth/models.py +49 -0
  36. ccproxy/auth/oauth/routes.py +396 -0
  37. ccproxy/auth/oauth/storage.py +0 -0
  38. ccproxy/auth/storage/__init__.py +12 -0
  39. ccproxy/auth/storage/base.py +57 -0
  40. ccproxy/auth/storage/json_file.py +159 -0
  41. ccproxy/auth/storage/keyring.py +192 -0
  42. ccproxy/claude_sdk/__init__.py +20 -0
  43. ccproxy/claude_sdk/client.py +169 -0
  44. ccproxy/claude_sdk/converter.py +331 -0
  45. ccproxy/claude_sdk/options.py +120 -0
  46. ccproxy/cli/__init__.py +14 -0
  47. ccproxy/cli/commands/__init__.py +8 -0
  48. ccproxy/cli/commands/auth.py +553 -0
  49. ccproxy/cli/commands/config/__init__.py +14 -0
  50. ccproxy/cli/commands/config/commands.py +766 -0
  51. ccproxy/cli/commands/config/schema_commands.py +119 -0
  52. ccproxy/cli/commands/serve.py +630 -0
  53. ccproxy/cli/docker/__init__.py +34 -0
  54. ccproxy/cli/docker/adapter_factory.py +157 -0
  55. ccproxy/cli/docker/params.py +278 -0
  56. ccproxy/cli/helpers.py +144 -0
  57. ccproxy/cli/main.py +193 -0
  58. ccproxy/cli/options/__init__.py +14 -0
  59. ccproxy/cli/options/claude_options.py +216 -0
  60. ccproxy/cli/options/core_options.py +40 -0
  61. ccproxy/cli/options/security_options.py +48 -0
  62. ccproxy/cli/options/server_options.py +117 -0
  63. ccproxy/config/__init__.py +40 -0
  64. ccproxy/config/auth.py +154 -0
  65. ccproxy/config/claude.py +124 -0
  66. ccproxy/config/cors.py +79 -0
  67. ccproxy/config/discovery.py +87 -0
  68. ccproxy/config/docker_settings.py +265 -0
  69. ccproxy/config/loader.py +108 -0
  70. ccproxy/config/observability.py +158 -0
  71. ccproxy/config/pricing.py +88 -0
  72. ccproxy/config/reverse_proxy.py +31 -0
  73. ccproxy/config/scheduler.py +89 -0
  74. ccproxy/config/security.py +14 -0
  75. ccproxy/config/server.py +81 -0
  76. ccproxy/config/settings.py +534 -0
  77. ccproxy/config/validators.py +231 -0
  78. ccproxy/core/__init__.py +274 -0
  79. ccproxy/core/async_utils.py +675 -0
  80. ccproxy/core/constants.py +97 -0
  81. ccproxy/core/errors.py +256 -0
  82. ccproxy/core/http.py +328 -0
  83. ccproxy/core/http_transformers.py +428 -0
  84. ccproxy/core/interfaces.py +247 -0
  85. ccproxy/core/logging.py +189 -0
  86. ccproxy/core/middleware.py +114 -0
  87. ccproxy/core/proxy.py +143 -0
  88. ccproxy/core/system.py +38 -0
  89. ccproxy/core/transformers.py +259 -0
  90. ccproxy/core/types.py +129 -0
  91. ccproxy/core/validators.py +288 -0
  92. ccproxy/docker/__init__.py +67 -0
  93. ccproxy/docker/adapter.py +588 -0
  94. ccproxy/docker/docker_path.py +207 -0
  95. ccproxy/docker/middleware.py +103 -0
  96. ccproxy/docker/models.py +228 -0
  97. ccproxy/docker/protocol.py +192 -0
  98. ccproxy/docker/stream_process.py +264 -0
  99. ccproxy/docker/validators.py +173 -0
  100. ccproxy/models/__init__.py +123 -0
  101. ccproxy/models/errors.py +42 -0
  102. ccproxy/models/messages.py +243 -0
  103. ccproxy/models/requests.py +85 -0
  104. ccproxy/models/responses.py +227 -0
  105. ccproxy/models/types.py +102 -0
  106. ccproxy/observability/__init__.py +51 -0
  107. ccproxy/observability/access_logger.py +400 -0
  108. ccproxy/observability/context.py +447 -0
  109. ccproxy/observability/metrics.py +539 -0
  110. ccproxy/observability/pushgateway.py +366 -0
  111. ccproxy/observability/sse_events.py +303 -0
  112. ccproxy/observability/stats_printer.py +755 -0
  113. ccproxy/observability/storage/__init__.py +1 -0
  114. ccproxy/observability/storage/duckdb_simple.py +665 -0
  115. ccproxy/observability/storage/models.py +55 -0
  116. ccproxy/pricing/__init__.py +19 -0
  117. ccproxy/pricing/cache.py +212 -0
  118. ccproxy/pricing/loader.py +267 -0
  119. ccproxy/pricing/models.py +106 -0
  120. ccproxy/pricing/updater.py +309 -0
  121. ccproxy/scheduler/__init__.py +39 -0
  122. ccproxy/scheduler/core.py +335 -0
  123. ccproxy/scheduler/exceptions.py +34 -0
  124. ccproxy/scheduler/manager.py +186 -0
  125. ccproxy/scheduler/registry.py +150 -0
  126. ccproxy/scheduler/tasks.py +484 -0
  127. ccproxy/services/__init__.py +10 -0
  128. ccproxy/services/claude_sdk_service.py +614 -0
  129. ccproxy/services/credentials/__init__.py +55 -0
  130. ccproxy/services/credentials/config.py +105 -0
  131. ccproxy/services/credentials/manager.py +562 -0
  132. ccproxy/services/credentials/oauth_client.py +482 -0
  133. ccproxy/services/proxy_service.py +1536 -0
  134. ccproxy/static/.keep +0 -0
  135. ccproxy/testing/__init__.py +34 -0
  136. ccproxy/testing/config.py +148 -0
  137. ccproxy/testing/content_generation.py +197 -0
  138. ccproxy/testing/mock_responses.py +262 -0
  139. ccproxy/testing/response_handlers.py +161 -0
  140. ccproxy/testing/scenarios.py +241 -0
  141. ccproxy/utils/__init__.py +6 -0
  142. ccproxy/utils/cost_calculator.py +210 -0
  143. ccproxy/utils/streaming_metrics.py +199 -0
  144. ccproxy_api-0.1.0.dist-info/METADATA +253 -0
  145. ccproxy_api-0.1.0.dist-info/RECORD +148 -0
  146. ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
  147. ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
  148. 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()