ccproxy-api 0.1.4__py3-none-any.whl → 0.1.6__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/_version.py +2 -2
- ccproxy/adapters/codex/__init__.py +11 -0
- ccproxy/adapters/openai/adapter.py +1 -1
- ccproxy/adapters/openai/models.py +1 -1
- ccproxy/adapters/openai/response_adapter.py +355 -0
- ccproxy/adapters/openai/response_models.py +178 -0
- ccproxy/adapters/openai/streaming.py +1 -0
- ccproxy/api/app.py +150 -224
- ccproxy/api/dependencies.py +22 -2
- ccproxy/api/middleware/errors.py +27 -3
- ccproxy/api/middleware/logging.py +4 -0
- ccproxy/api/responses.py +6 -1
- ccproxy/api/routes/claude.py +222 -17
- ccproxy/api/routes/codex.py +1231 -0
- ccproxy/api/routes/health.py +228 -3
- ccproxy/api/routes/proxy.py +25 -6
- ccproxy/api/services/permission_service.py +2 -2
- ccproxy/auth/openai/__init__.py +13 -0
- ccproxy/auth/openai/credentials.py +166 -0
- ccproxy/auth/openai/oauth_client.py +334 -0
- ccproxy/auth/openai/storage.py +184 -0
- ccproxy/claude_sdk/__init__.py +4 -8
- ccproxy/claude_sdk/client.py +661 -131
- ccproxy/claude_sdk/exceptions.py +16 -0
- ccproxy/claude_sdk/manager.py +219 -0
- ccproxy/claude_sdk/message_queue.py +342 -0
- ccproxy/claude_sdk/options.py +6 -1
- ccproxy/claude_sdk/session_client.py +546 -0
- ccproxy/claude_sdk/session_pool.py +550 -0
- ccproxy/claude_sdk/stream_handle.py +538 -0
- ccproxy/claude_sdk/stream_worker.py +392 -0
- ccproxy/claude_sdk/streaming.py +53 -11
- ccproxy/cli/commands/auth.py +398 -1
- ccproxy/cli/commands/serve.py +99 -1
- ccproxy/cli/options/claude_options.py +47 -0
- ccproxy/config/__init__.py +0 -3
- ccproxy/config/claude.py +171 -23
- ccproxy/config/codex.py +100 -0
- ccproxy/config/discovery.py +10 -1
- ccproxy/config/scheduler.py +2 -2
- ccproxy/config/settings.py +38 -1
- ccproxy/core/codex_transformers.py +389 -0
- ccproxy/core/http_transformers.py +458 -75
- ccproxy/core/logging.py +108 -12
- ccproxy/core/transformers.py +5 -0
- ccproxy/models/claude_sdk.py +57 -0
- ccproxy/models/detection.py +208 -0
- ccproxy/models/requests.py +22 -0
- ccproxy/models/responses.py +16 -0
- ccproxy/observability/access_logger.py +72 -14
- ccproxy/observability/metrics.py +151 -0
- ccproxy/observability/storage/duckdb_simple.py +12 -0
- ccproxy/observability/storage/models.py +16 -0
- ccproxy/observability/streaming_response.py +107 -0
- ccproxy/scheduler/manager.py +31 -6
- ccproxy/scheduler/tasks.py +122 -0
- ccproxy/services/claude_detection_service.py +269 -0
- ccproxy/services/claude_sdk_service.py +333 -130
- ccproxy/services/codex_detection_service.py +263 -0
- ccproxy/services/proxy_service.py +618 -197
- ccproxy/utils/__init__.py +9 -1
- ccproxy/utils/disconnection_monitor.py +83 -0
- ccproxy/utils/id_generator.py +12 -0
- ccproxy/utils/model_mapping.py +7 -5
- ccproxy/utils/startup_helpers.py +470 -0
- ccproxy_api-0.1.6.dist-info/METADATA +615 -0
- {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.6.dist-info}/RECORD +70 -47
- ccproxy/config/loader.py +0 -105
- ccproxy_api-0.1.4.dist-info/METADATA +0 -369
- {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.6.dist-info}/WHEEL +0 -0
- {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.6.dist-info}/entry_points.txt +0 -0
- {ccproxy_api-0.1.4.dist-info → ccproxy_api-0.1.6.dist-info}/licenses/LICENSE +0 -0
ccproxy/observability/metrics.py
CHANGED
|
@@ -205,6 +205,62 @@ class PrometheusMetrics:
|
|
|
205
205
|
registry=self.registry,
|
|
206
206
|
)
|
|
207
207
|
|
|
208
|
+
# Claude SDK Pool metrics
|
|
209
|
+
self.pool_clients_total = Gauge(
|
|
210
|
+
f"{self.namespace}_pool_clients_total",
|
|
211
|
+
"Total number of clients in the pool",
|
|
212
|
+
registry=self.registry,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
self.pool_clients_available = Gauge(
|
|
216
|
+
f"{self.namespace}_pool_clients_available",
|
|
217
|
+
"Number of available clients in the pool",
|
|
218
|
+
registry=self.registry,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
self.pool_clients_active = Gauge(
|
|
222
|
+
f"{self.namespace}_pool_clients_active",
|
|
223
|
+
"Number of active clients currently processing requests",
|
|
224
|
+
registry=self.registry,
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
self.pool_connections_created_total = Counter(
|
|
228
|
+
f"{self.namespace}_pool_connections_created_total",
|
|
229
|
+
"Total number of pool connections created",
|
|
230
|
+
registry=self.registry,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
self.pool_connections_closed_total = Counter(
|
|
234
|
+
f"{self.namespace}_pool_connections_closed_total",
|
|
235
|
+
"Total number of pool connections closed",
|
|
236
|
+
registry=self.registry,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
self.pool_acquisitions_total = Counter(
|
|
240
|
+
f"{self.namespace}_pool_acquisitions_total",
|
|
241
|
+
"Total number of client acquisitions from pool",
|
|
242
|
+
registry=self.registry,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
self.pool_releases_total = Counter(
|
|
246
|
+
f"{self.namespace}_pool_releases_total",
|
|
247
|
+
"Total number of client releases to pool",
|
|
248
|
+
registry=self.registry,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
self.pool_health_check_failures_total = Counter(
|
|
252
|
+
f"{self.namespace}_pool_health_check_failures_total",
|
|
253
|
+
"Total number of pool health check failures",
|
|
254
|
+
registry=self.registry,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
self.pool_acquisition_duration = Histogram(
|
|
258
|
+
f"{self.namespace}_pool_acquisition_duration_seconds",
|
|
259
|
+
"Time taken to acquire a client from the pool",
|
|
260
|
+
buckets=[0.001, 0.005, 0.01, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0],
|
|
261
|
+
registry=self.registry,
|
|
262
|
+
)
|
|
263
|
+
|
|
208
264
|
# Set initial system info
|
|
209
265
|
try:
|
|
210
266
|
from ccproxy import __version__
|
|
@@ -468,6 +524,101 @@ class PrometheusMetrics:
|
|
|
468
524
|
and self._pushgateway_client.is_enabled()
|
|
469
525
|
)
|
|
470
526
|
|
|
527
|
+
# Claude SDK Pool metrics methods
|
|
528
|
+
|
|
529
|
+
def update_pool_gauges(
|
|
530
|
+
self,
|
|
531
|
+
total_clients: int,
|
|
532
|
+
available_clients: int,
|
|
533
|
+
active_clients: int,
|
|
534
|
+
) -> None:
|
|
535
|
+
"""
|
|
536
|
+
Update pool gauge metrics (current state).
|
|
537
|
+
|
|
538
|
+
Args:
|
|
539
|
+
total_clients: Total number of clients in pool
|
|
540
|
+
available_clients: Number of available clients
|
|
541
|
+
active_clients: Number of active clients
|
|
542
|
+
"""
|
|
543
|
+
if not self._enabled:
|
|
544
|
+
return
|
|
545
|
+
|
|
546
|
+
# Update gauges
|
|
547
|
+
self.pool_clients_total.set(total_clients)
|
|
548
|
+
self.pool_clients_available.set(available_clients)
|
|
549
|
+
self.pool_clients_active.set(active_clients)
|
|
550
|
+
|
|
551
|
+
# Note: Counters are managed directly by the pool operations
|
|
552
|
+
# This method only updates the current gauges
|
|
553
|
+
|
|
554
|
+
def record_pool_acquisition_time(self, duration_seconds: float) -> None:
|
|
555
|
+
"""
|
|
556
|
+
Record the time taken to acquire a client from the pool.
|
|
557
|
+
|
|
558
|
+
Args:
|
|
559
|
+
duration_seconds: Time in seconds to acquire client
|
|
560
|
+
"""
|
|
561
|
+
if not self._enabled:
|
|
562
|
+
return
|
|
563
|
+
|
|
564
|
+
self.pool_acquisition_duration.observe(duration_seconds)
|
|
565
|
+
|
|
566
|
+
def inc_pool_connections_created(self) -> None:
|
|
567
|
+
"""Increment the pool connections created counter."""
|
|
568
|
+
if not self._enabled:
|
|
569
|
+
return
|
|
570
|
+
|
|
571
|
+
self.pool_connections_created_total.inc()
|
|
572
|
+
|
|
573
|
+
def inc_pool_connections_closed(self) -> None:
|
|
574
|
+
"""Increment the pool connections closed counter."""
|
|
575
|
+
if not self._enabled:
|
|
576
|
+
return
|
|
577
|
+
|
|
578
|
+
self.pool_connections_closed_total.inc()
|
|
579
|
+
|
|
580
|
+
def inc_pool_acquisitions(self) -> None:
|
|
581
|
+
"""Increment the pool acquisitions counter."""
|
|
582
|
+
if not self._enabled:
|
|
583
|
+
return
|
|
584
|
+
|
|
585
|
+
self.pool_acquisitions_total.inc()
|
|
586
|
+
|
|
587
|
+
def inc_pool_releases(self) -> None:
|
|
588
|
+
"""Increment the pool releases counter."""
|
|
589
|
+
if not self._enabled:
|
|
590
|
+
return
|
|
591
|
+
|
|
592
|
+
self.pool_releases_total.inc()
|
|
593
|
+
|
|
594
|
+
def inc_pool_health_check_failures(self) -> None:
|
|
595
|
+
"""Increment the pool health check failures counter."""
|
|
596
|
+
if not self._enabled:
|
|
597
|
+
return
|
|
598
|
+
|
|
599
|
+
self.pool_health_check_failures_total.inc()
|
|
600
|
+
|
|
601
|
+
def set_pool_clients_total(self, count: int) -> None:
|
|
602
|
+
"""Set the total number of clients in the pool."""
|
|
603
|
+
if not self._enabled:
|
|
604
|
+
return
|
|
605
|
+
|
|
606
|
+
self.pool_clients_total.set(count)
|
|
607
|
+
|
|
608
|
+
def set_pool_clients_available(self, count: int) -> None:
|
|
609
|
+
"""Set the number of available clients in the pool."""
|
|
610
|
+
if not self._enabled:
|
|
611
|
+
return
|
|
612
|
+
|
|
613
|
+
self.pool_clients_available.set(count)
|
|
614
|
+
|
|
615
|
+
def set_pool_clients_active(self, count: int) -> None:
|
|
616
|
+
"""Set the number of active clients in the pool."""
|
|
617
|
+
if not self._enabled:
|
|
618
|
+
return
|
|
619
|
+
|
|
620
|
+
self.pool_clients_active.set(count)
|
|
621
|
+
|
|
471
622
|
|
|
472
623
|
# Global metrics instance
|
|
473
624
|
_global_metrics: PrometheusMetrics | None = None
|
|
@@ -60,6 +60,18 @@ class AccessLogPayload(TypedDict, total=False):
|
|
|
60
60
|
cache_write_tokens: int
|
|
61
61
|
cost_usd: float
|
|
62
62
|
cost_sdk_usd: float
|
|
63
|
+
num_turns: int # number of conversation turns
|
|
64
|
+
|
|
65
|
+
# Session context metadata
|
|
66
|
+
session_type: str # "session_pool" or "direct"
|
|
67
|
+
session_status: str # active, idle, connecting, etc.
|
|
68
|
+
session_age_seconds: float # how long session has been alive
|
|
69
|
+
session_message_count: int # number of messages in session
|
|
70
|
+
session_client_id: str # unique session client identifier
|
|
71
|
+
session_pool_enabled: bool # whether session pooling is enabled
|
|
72
|
+
session_idle_seconds: float # how long since last activity
|
|
73
|
+
session_error_count: int # number of errors in this session
|
|
74
|
+
session_is_new: bool # whether this is a newly created session
|
|
63
75
|
|
|
64
76
|
|
|
65
77
|
class SimpleDuckDBStorage:
|
|
@@ -44,6 +44,22 @@ class AccessLog(SQLModel, table=True):
|
|
|
44
44
|
cache_write_tokens: int = Field(default=0)
|
|
45
45
|
cost_usd: float = Field(default=0.0)
|
|
46
46
|
cost_sdk_usd: float = Field(default=0.0)
|
|
47
|
+
num_turns: int = Field(default=0) # number of conversation turns
|
|
48
|
+
|
|
49
|
+
# Session context metadata
|
|
50
|
+
session_type: str = Field(default="") # "session_pool" or "direct"
|
|
51
|
+
session_status: str = Field(default="") # active, idle, connecting, etc.
|
|
52
|
+
session_age_seconds: float = Field(default=0.0) # how long session has been alive
|
|
53
|
+
session_message_count: int = Field(default=0) # number of messages in session
|
|
54
|
+
session_client_id: str = Field(default="") # unique session client identifier
|
|
55
|
+
session_pool_enabled: bool = Field(
|
|
56
|
+
default=False
|
|
57
|
+
) # whether session pooling is enabled
|
|
58
|
+
session_idle_seconds: float = Field(default=0.0) # how long since last activity
|
|
59
|
+
session_error_count: int = Field(default=0) # number of errors in this session
|
|
60
|
+
session_is_new: bool = Field(
|
|
61
|
+
default=True
|
|
62
|
+
) # whether this is a newly created session
|
|
47
63
|
|
|
48
64
|
class Config:
|
|
49
65
|
"""SQLModel configuration."""
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""FastAPI StreamingResponse with automatic access logging on completion.
|
|
2
|
+
|
|
3
|
+
This module provides a reusable StreamingResponseWithLogging class that wraps
|
|
4
|
+
any async generator and handles access logging when the stream completes,
|
|
5
|
+
eliminating code duplication between different streaming endpoints.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import AsyncGenerator, AsyncIterator
|
|
11
|
+
from typing import TYPE_CHECKING, Any
|
|
12
|
+
|
|
13
|
+
import structlog
|
|
14
|
+
from fastapi.responses import StreamingResponse
|
|
15
|
+
|
|
16
|
+
from ccproxy.observability.access_logger import log_request_access
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from ccproxy.observability.context import RequestContext
|
|
21
|
+
from ccproxy.observability.metrics import PrometheusMetrics
|
|
22
|
+
|
|
23
|
+
logger = structlog.get_logger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StreamingResponseWithLogging(StreamingResponse):
|
|
27
|
+
"""FastAPI StreamingResponse that triggers access logging on completion.
|
|
28
|
+
|
|
29
|
+
This class wraps a streaming response generator to automatically trigger
|
|
30
|
+
access logging when the stream completes (either successfully or with an error).
|
|
31
|
+
This eliminates the need for manual access logging in individual stream processors.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
content: AsyncGenerator[bytes, None] | AsyncIterator[bytes],
|
|
37
|
+
request_context: RequestContext,
|
|
38
|
+
metrics: PrometheusMetrics | None = None,
|
|
39
|
+
status_code: int = 200,
|
|
40
|
+
**kwargs: Any,
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Initialize streaming response with logging capability.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
content: The async generator producing streaming content
|
|
46
|
+
request_context: The request context for access logging
|
|
47
|
+
metrics: Optional PrometheusMetrics instance for recording metrics
|
|
48
|
+
status_code: HTTP status code for the response
|
|
49
|
+
**kwargs: Additional arguments passed to StreamingResponse
|
|
50
|
+
"""
|
|
51
|
+
# Wrap the content generator to add logging
|
|
52
|
+
logged_content = self._wrap_with_logging(
|
|
53
|
+
content, request_context, metrics, status_code
|
|
54
|
+
)
|
|
55
|
+
super().__init__(logged_content, status_code=status_code, **kwargs)
|
|
56
|
+
|
|
57
|
+
async def _wrap_with_logging(
|
|
58
|
+
self,
|
|
59
|
+
content: AsyncGenerator[bytes, None] | AsyncIterator[bytes],
|
|
60
|
+
context: RequestContext,
|
|
61
|
+
metrics: PrometheusMetrics | None,
|
|
62
|
+
status_code: int,
|
|
63
|
+
) -> AsyncGenerator[bytes, None]:
|
|
64
|
+
"""Wrap content generator with access logging on completion.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
content: The original content generator
|
|
68
|
+
context: Request context for logging
|
|
69
|
+
metrics: Optional metrics instance
|
|
70
|
+
status_code: HTTP status code
|
|
71
|
+
|
|
72
|
+
Yields:
|
|
73
|
+
bytes: Content chunks from the original generator
|
|
74
|
+
"""
|
|
75
|
+
try:
|
|
76
|
+
# Stream all content from the original generator
|
|
77
|
+
async for chunk in content:
|
|
78
|
+
yield chunk
|
|
79
|
+
except GeneratorExit:
|
|
80
|
+
# Client disconnected - log this and re-raise to propagate to underlying generators
|
|
81
|
+
logger.info(
|
|
82
|
+
"streaming_response_client_disconnected",
|
|
83
|
+
request_id=context.request_id,
|
|
84
|
+
message="Client disconnected from streaming response, propagating GeneratorExit",
|
|
85
|
+
)
|
|
86
|
+
# CRITICAL: Re-raise GeneratorExit to propagate disconnect to create_listener()
|
|
87
|
+
raise
|
|
88
|
+
finally:
|
|
89
|
+
# Log access when stream completes (success or error)
|
|
90
|
+
try:
|
|
91
|
+
# Add streaming completion event type to context
|
|
92
|
+
context.add_metadata(event_type="streaming_complete")
|
|
93
|
+
|
|
94
|
+
# Check if status_code was updated in context metadata (e.g., due to error)
|
|
95
|
+
final_status_code = context.metadata.get("status_code", status_code)
|
|
96
|
+
|
|
97
|
+
await log_request_access(
|
|
98
|
+
context=context,
|
|
99
|
+
status_code=final_status_code,
|
|
100
|
+
metrics=metrics,
|
|
101
|
+
)
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.warning(
|
|
104
|
+
"streaming_access_log_failed",
|
|
105
|
+
error=str(e),
|
|
106
|
+
request_id=context.request_id,
|
|
107
|
+
)
|
ccproxy/scheduler/manager.py
CHANGED
|
@@ -7,6 +7,7 @@ from ccproxy.config.settings import Settings
|
|
|
7
7
|
from .core import Scheduler
|
|
8
8
|
from .registry import register_task
|
|
9
9
|
from .tasks import (
|
|
10
|
+
PoolStatsTask,
|
|
10
11
|
PricingCacheUpdateTask,
|
|
11
12
|
PushgatewayTask,
|
|
12
13
|
StatsPrintingTask,
|
|
@@ -31,6 +32,19 @@ async def setup_scheduler_tasks(scheduler: Scheduler, settings: Settings) -> Non
|
|
|
31
32
|
logger.info("scheduler_disabled")
|
|
32
33
|
return
|
|
33
34
|
|
|
35
|
+
# Log network features status
|
|
36
|
+
logger.info(
|
|
37
|
+
"network_features_status",
|
|
38
|
+
pricing_updates_enabled=scheduler_config.pricing_update_enabled,
|
|
39
|
+
version_check_enabled=scheduler_config.version_check_enabled,
|
|
40
|
+
message=(
|
|
41
|
+
"Network features disabled by default for privacy"
|
|
42
|
+
if not scheduler_config.pricing_update_enabled
|
|
43
|
+
and not scheduler_config.version_check_enabled
|
|
44
|
+
else "Some network features are enabled"
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
|
|
34
48
|
# Add pushgateway task if enabled
|
|
35
49
|
if scheduler_config.pushgateway_enabled:
|
|
36
50
|
try:
|
|
@@ -123,21 +137,32 @@ async def setup_scheduler_tasks(scheduler: Scheduler, settings: Settings) -> Non
|
|
|
123
137
|
)
|
|
124
138
|
|
|
125
139
|
|
|
126
|
-
def _register_default_tasks() -> None:
|
|
127
|
-
"""Register default task types in the global registry."""
|
|
140
|
+
def _register_default_tasks(settings: Settings) -> None:
|
|
141
|
+
"""Register default task types in the global registry based on configuration."""
|
|
128
142
|
from .registry import get_task_registry
|
|
129
143
|
|
|
130
144
|
registry = get_task_registry()
|
|
145
|
+
scheduler_config = settings.scheduler
|
|
131
146
|
|
|
132
|
-
# Only register
|
|
133
|
-
if not registry.is_registered(
|
|
147
|
+
# Only register pushgateway task if enabled
|
|
148
|
+
if scheduler_config.pushgateway_enabled and not registry.is_registered(
|
|
149
|
+
"pushgateway"
|
|
150
|
+
):
|
|
134
151
|
register_task("pushgateway", PushgatewayTask)
|
|
135
|
-
|
|
152
|
+
|
|
153
|
+
# Only register stats printing task if enabled
|
|
154
|
+
if scheduler_config.stats_printing_enabled and not registry.is_registered(
|
|
155
|
+
"stats_printing"
|
|
156
|
+
):
|
|
136
157
|
register_task("stats_printing", StatsPrintingTask)
|
|
158
|
+
|
|
159
|
+
# Always register core tasks (not metrics-related)
|
|
137
160
|
if not registry.is_registered("pricing_cache_update"):
|
|
138
161
|
register_task("pricing_cache_update", PricingCacheUpdateTask)
|
|
139
162
|
if not registry.is_registered("version_update_check"):
|
|
140
163
|
register_task("version_update_check", VersionUpdateCheckTask)
|
|
164
|
+
if not registry.is_registered("pool_stats"):
|
|
165
|
+
register_task("pool_stats", PoolStatsTask)
|
|
141
166
|
|
|
142
167
|
|
|
143
168
|
async def start_scheduler(settings: Settings) -> Scheduler | None:
|
|
@@ -156,7 +181,7 @@ async def start_scheduler(settings: Settings) -> Scheduler | None:
|
|
|
156
181
|
return None
|
|
157
182
|
|
|
158
183
|
# Register task types (only when actually starting scheduler)
|
|
159
|
-
_register_default_tasks()
|
|
184
|
+
_register_default_tasks(settings)
|
|
160
185
|
|
|
161
186
|
# Create scheduler with settings
|
|
162
187
|
scheduler = Scheduler(
|
ccproxy/scheduler/tasks.py
CHANGED
|
@@ -483,6 +483,128 @@ class PricingCacheUpdateTask(BaseScheduledTask):
|
|
|
483
483
|
return False
|
|
484
484
|
|
|
485
485
|
|
|
486
|
+
class PoolStatsTask(BaseScheduledTask):
|
|
487
|
+
"""Task for displaying pool statistics periodically."""
|
|
488
|
+
|
|
489
|
+
def __init__(
|
|
490
|
+
self,
|
|
491
|
+
name: str,
|
|
492
|
+
interval_seconds: float,
|
|
493
|
+
enabled: bool = True,
|
|
494
|
+
pool_manager: Any | None = None,
|
|
495
|
+
):
|
|
496
|
+
"""
|
|
497
|
+
Initialize pool stats task.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
name: Task name
|
|
501
|
+
interval_seconds: Interval between stats display
|
|
502
|
+
enabled: Whether task is enabled
|
|
503
|
+
pool_manager: Injected pool manager instance
|
|
504
|
+
"""
|
|
505
|
+
super().__init__(
|
|
506
|
+
name=name,
|
|
507
|
+
interval_seconds=interval_seconds,
|
|
508
|
+
enabled=enabled,
|
|
509
|
+
)
|
|
510
|
+
self._pool_manager = pool_manager
|
|
511
|
+
|
|
512
|
+
async def setup(self) -> None:
|
|
513
|
+
"""Initialize pool manager instance if not injected."""
|
|
514
|
+
if self._pool_manager is None:
|
|
515
|
+
logger.warning(
|
|
516
|
+
"pool_stats_task_no_manager",
|
|
517
|
+
task_name=self.name,
|
|
518
|
+
message="Pool manager not injected, task will be disabled",
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
async def run(self) -> bool:
|
|
522
|
+
"""Display pool statistics."""
|
|
523
|
+
try:
|
|
524
|
+
if not self._pool_manager:
|
|
525
|
+
return True # Not an error, just no pool manager available
|
|
526
|
+
|
|
527
|
+
# Get general pool stats (if available)
|
|
528
|
+
general_pool = getattr(self._pool_manager, "_pool", None)
|
|
529
|
+
general_stats = None
|
|
530
|
+
if general_pool:
|
|
531
|
+
general_stats = general_pool.get_stats()
|
|
532
|
+
|
|
533
|
+
# Get session pool stats
|
|
534
|
+
session_pool = getattr(self._pool_manager, "_session_pool", None)
|
|
535
|
+
session_stats = None
|
|
536
|
+
if session_pool:
|
|
537
|
+
session_stats = await session_pool.get_stats()
|
|
538
|
+
|
|
539
|
+
# Log pool statistics
|
|
540
|
+
logger.debug(
|
|
541
|
+
"pool_stats_report",
|
|
542
|
+
task_name=self.name,
|
|
543
|
+
general_pool={
|
|
544
|
+
"enabled": bool(general_pool),
|
|
545
|
+
"total_clients": general_stats.total_clients
|
|
546
|
+
if general_stats
|
|
547
|
+
else 0,
|
|
548
|
+
"available_clients": general_stats.available_clients
|
|
549
|
+
if general_stats
|
|
550
|
+
else 0,
|
|
551
|
+
"active_clients": general_stats.active_clients
|
|
552
|
+
if general_stats
|
|
553
|
+
else 0,
|
|
554
|
+
"connections_created": general_stats.connections_created
|
|
555
|
+
if general_stats
|
|
556
|
+
else 0,
|
|
557
|
+
"connections_closed": general_stats.connections_closed
|
|
558
|
+
if general_stats
|
|
559
|
+
else 0,
|
|
560
|
+
"acquire_count": general_stats.acquire_count
|
|
561
|
+
if general_stats
|
|
562
|
+
else 0,
|
|
563
|
+
"release_count": general_stats.release_count
|
|
564
|
+
if general_stats
|
|
565
|
+
else 0,
|
|
566
|
+
"health_check_failures": general_stats.health_check_failures
|
|
567
|
+
if general_stats
|
|
568
|
+
else 0,
|
|
569
|
+
}
|
|
570
|
+
if general_pool
|
|
571
|
+
else None,
|
|
572
|
+
session_pool={
|
|
573
|
+
"enabled": session_stats.get("enabled", False)
|
|
574
|
+
if session_stats
|
|
575
|
+
else False,
|
|
576
|
+
"total_sessions": session_stats.get("total_sessions", 0)
|
|
577
|
+
if session_stats
|
|
578
|
+
else 0,
|
|
579
|
+
"active_sessions": session_stats.get("active_sessions", 0)
|
|
580
|
+
if session_stats
|
|
581
|
+
else 0,
|
|
582
|
+
"max_sessions": session_stats.get("max_sessions", 0)
|
|
583
|
+
if session_stats
|
|
584
|
+
else 0,
|
|
585
|
+
"total_messages": session_stats.get("total_messages", 0)
|
|
586
|
+
if session_stats
|
|
587
|
+
else 0,
|
|
588
|
+
"session_ttl": session_stats.get("session_ttl", 0)
|
|
589
|
+
if session_stats
|
|
590
|
+
else 0,
|
|
591
|
+
}
|
|
592
|
+
if session_pool
|
|
593
|
+
else None,
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
return True
|
|
597
|
+
|
|
598
|
+
except Exception as e:
|
|
599
|
+
logger.error(
|
|
600
|
+
"pool_stats_task_error",
|
|
601
|
+
task_name=self.name,
|
|
602
|
+
error=str(e),
|
|
603
|
+
error_type=type(e).__name__,
|
|
604
|
+
)
|
|
605
|
+
return False
|
|
606
|
+
|
|
607
|
+
|
|
486
608
|
class VersionUpdateCheckTask(BaseScheduledTask):
|
|
487
609
|
"""Task for checking version updates periodically."""
|
|
488
610
|
|