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,447 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Request context management with timing and correlation.
|
|
3
|
+
|
|
4
|
+
This module provides context managers and utilities for tracking request lifecycle,
|
|
5
|
+
timing measurements, and correlation across async operations. Uses structlog for
|
|
6
|
+
rich business event logging.
|
|
7
|
+
|
|
8
|
+
Key features:
|
|
9
|
+
- Accurate timing measurement using time.perf_counter()
|
|
10
|
+
- Request correlation with unique IDs
|
|
11
|
+
- Structured logging integration
|
|
12
|
+
- Async-safe context management
|
|
13
|
+
- Exception handling and error tracking
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import asyncio
|
|
19
|
+
import time
|
|
20
|
+
import uuid
|
|
21
|
+
from collections.abc import AsyncGenerator
|
|
22
|
+
from contextlib import asynccontextmanager
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from typing import Any, Optional
|
|
25
|
+
|
|
26
|
+
import structlog
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
logger = structlog.get_logger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class RequestContext:
|
|
34
|
+
"""
|
|
35
|
+
Context object for tracking request state and metadata.
|
|
36
|
+
|
|
37
|
+
Provides access to request ID, timing information, and structured logger
|
|
38
|
+
with automatically injected context.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
request_id: str
|
|
42
|
+
start_time: float
|
|
43
|
+
logger: structlog.BoundLogger
|
|
44
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
45
|
+
storage: Any | None = None # Optional DuckDB storage instance
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def duration_ms(self) -> float:
|
|
49
|
+
"""Get current duration in milliseconds."""
|
|
50
|
+
return (time.perf_counter() - self.start_time) * 1000
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def duration_seconds(self) -> float:
|
|
54
|
+
"""Get current duration in seconds."""
|
|
55
|
+
return time.perf_counter() - self.start_time
|
|
56
|
+
|
|
57
|
+
def add_metadata(self, **kwargs: Any) -> None:
|
|
58
|
+
"""Add metadata to the request context."""
|
|
59
|
+
self.metadata.update(kwargs)
|
|
60
|
+
# Update logger context
|
|
61
|
+
self.logger = self.logger.bind(**kwargs)
|
|
62
|
+
|
|
63
|
+
def log_event(self, event: str, **kwargs: Any) -> None:
|
|
64
|
+
"""Log an event with current context and timing."""
|
|
65
|
+
self.logger.info(
|
|
66
|
+
event, request_id=self.request_id, duration_ms=self.duration_ms, **kwargs
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@asynccontextmanager
|
|
71
|
+
async def request_context(
|
|
72
|
+
request_id: str | None = None,
|
|
73
|
+
storage: Any | None = None,
|
|
74
|
+
metrics: Any | None = None,
|
|
75
|
+
**initial_context: Any,
|
|
76
|
+
) -> AsyncGenerator[RequestContext, None]:
|
|
77
|
+
"""
|
|
78
|
+
Context manager for tracking complete request lifecycle with timing.
|
|
79
|
+
|
|
80
|
+
Automatically logs request start/success/error events with accurate timing.
|
|
81
|
+
Provides structured logging with request correlation.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
request_id: Unique request identifier (generated if not provided)
|
|
85
|
+
storage: Optional storage backend for access logs
|
|
86
|
+
metrics: Optional PrometheusMetrics instance for active request tracking
|
|
87
|
+
**initial_context: Initial context to include in all log events
|
|
88
|
+
|
|
89
|
+
Yields:
|
|
90
|
+
RequestContext: Context object with timing and logging capabilities
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
async with request_context(method="POST", path="/v1/messages") as ctx:
|
|
94
|
+
ctx.add_metadata(model="claude-3-5-sonnet")
|
|
95
|
+
# Process request
|
|
96
|
+
ctx.log_event("request_processed", tokens=150)
|
|
97
|
+
# Context automatically logs success with timing
|
|
98
|
+
"""
|
|
99
|
+
if request_id is None:
|
|
100
|
+
request_id = str(uuid.uuid4())
|
|
101
|
+
|
|
102
|
+
# Create logger with bound context
|
|
103
|
+
request_logger = logger.bind(request_id=request_id, **initial_context)
|
|
104
|
+
|
|
105
|
+
# Record start time
|
|
106
|
+
start_time = time.perf_counter()
|
|
107
|
+
|
|
108
|
+
# Log request start
|
|
109
|
+
request_logger.debug(
|
|
110
|
+
"request_start", request_id=request_id, timestamp=time.time(), **initial_context
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Emit SSE event for real-time dashboard updates
|
|
114
|
+
await _emit_request_start_event(request_id, initial_context)
|
|
115
|
+
|
|
116
|
+
# Increment active requests if metrics provided
|
|
117
|
+
if metrics:
|
|
118
|
+
metrics.inc_active_requests()
|
|
119
|
+
|
|
120
|
+
# Create context object
|
|
121
|
+
ctx = RequestContext(
|
|
122
|
+
request_id=request_id,
|
|
123
|
+
start_time=start_time,
|
|
124
|
+
logger=request_logger,
|
|
125
|
+
metadata=dict(initial_context),
|
|
126
|
+
storage=storage,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
yield ctx
|
|
131
|
+
|
|
132
|
+
# Log successful completion with comprehensive access log
|
|
133
|
+
duration_ms = ctx.duration_ms
|
|
134
|
+
|
|
135
|
+
# Use the new unified access logger for comprehensive logging
|
|
136
|
+
from ccproxy.observability.access_logger import log_request_access
|
|
137
|
+
|
|
138
|
+
await log_request_access(
|
|
139
|
+
context=ctx,
|
|
140
|
+
# Extract client info from metadata if available
|
|
141
|
+
client_ip=ctx.metadata.get("client_ip"),
|
|
142
|
+
user_agent=ctx.metadata.get("user_agent"),
|
|
143
|
+
query=ctx.metadata.get("query"),
|
|
144
|
+
storage=ctx.storage, # Pass storage from context
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Also keep the original request_success event for debugging
|
|
148
|
+
request_logger.debug(
|
|
149
|
+
"request_success",
|
|
150
|
+
request_id=request_id,
|
|
151
|
+
duration_ms=duration_ms,
|
|
152
|
+
duration_seconds=ctx.duration_seconds,
|
|
153
|
+
**ctx.metadata,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
# Log error with timing
|
|
158
|
+
duration_ms = ctx.duration_ms
|
|
159
|
+
error_type = type(e).__name__
|
|
160
|
+
|
|
161
|
+
request_logger.error(
|
|
162
|
+
"request_error",
|
|
163
|
+
request_id=request_id,
|
|
164
|
+
duration_ms=duration_ms,
|
|
165
|
+
duration_seconds=ctx.duration_seconds,
|
|
166
|
+
error_type=error_type,
|
|
167
|
+
error_message=str(e),
|
|
168
|
+
**ctx.metadata,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Emit SSE event for real-time dashboard updates
|
|
172
|
+
await _emit_request_error_event(request_id, error_type, str(e), ctx.metadata)
|
|
173
|
+
|
|
174
|
+
# Re-raise the exception
|
|
175
|
+
raise
|
|
176
|
+
finally:
|
|
177
|
+
# Decrement active requests if metrics provided
|
|
178
|
+
if metrics:
|
|
179
|
+
metrics.dec_active_requests()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@asynccontextmanager
|
|
183
|
+
async def timed_operation(
|
|
184
|
+
operation_name: str, request_id: str | None = None, **context: Any
|
|
185
|
+
) -> AsyncGenerator[dict[str, Any], None]:
|
|
186
|
+
"""
|
|
187
|
+
Context manager for timing individual operations within a request.
|
|
188
|
+
|
|
189
|
+
Useful for measuring specific parts of request processing like
|
|
190
|
+
API calls, database queries, or data processing steps.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
operation_name: Name of the operation being timed
|
|
194
|
+
request_id: Associated request ID for correlation
|
|
195
|
+
**context: Additional context for logging
|
|
196
|
+
|
|
197
|
+
Yields:
|
|
198
|
+
Dict with timing information and logger
|
|
199
|
+
|
|
200
|
+
Example:
|
|
201
|
+
async with timed_operation("claude_api_call", request_id=ctx.request_id) as op:
|
|
202
|
+
response = await api_client.call()
|
|
203
|
+
op["response_size"] = len(response)
|
|
204
|
+
# Automatically logs operation timing
|
|
205
|
+
"""
|
|
206
|
+
start_time = time.perf_counter()
|
|
207
|
+
operation_id = str(uuid.uuid4())
|
|
208
|
+
|
|
209
|
+
# Create operation logger
|
|
210
|
+
op_logger = logger.bind(
|
|
211
|
+
operation_name=operation_name,
|
|
212
|
+
operation_id=operation_id,
|
|
213
|
+
request_id=request_id,
|
|
214
|
+
**context,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Log operation start (only for important operations)
|
|
218
|
+
if operation_name in ("claude_api_call", "request_processing", "auth_check"):
|
|
219
|
+
op_logger.debug(
|
|
220
|
+
"operation_start",
|
|
221
|
+
operation_name=operation_name,
|
|
222
|
+
**context,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
# Operation context
|
|
226
|
+
op_context = {
|
|
227
|
+
"operation_id": operation_id,
|
|
228
|
+
"logger": op_logger,
|
|
229
|
+
"start_time": start_time,
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
yield op_context
|
|
234
|
+
|
|
235
|
+
# Log successful completion (only for important operations)
|
|
236
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
237
|
+
if operation_name in ("claude_api_call", "request_processing", "auth_check"):
|
|
238
|
+
op_logger.info(
|
|
239
|
+
"operation_success",
|
|
240
|
+
operation_name=operation_name,
|
|
241
|
+
duration_ms=duration_ms,
|
|
242
|
+
**{
|
|
243
|
+
k: v
|
|
244
|
+
for k, v in op_context.items()
|
|
245
|
+
if k not in ("logger", "start_time")
|
|
246
|
+
},
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
except Exception as e:
|
|
250
|
+
# Log operation error
|
|
251
|
+
duration_ms = (time.perf_counter() - start_time) * 1000
|
|
252
|
+
error_type = type(e).__name__
|
|
253
|
+
|
|
254
|
+
op_logger.error(
|
|
255
|
+
"operation_error",
|
|
256
|
+
operation_name=operation_name,
|
|
257
|
+
duration_ms=duration_ms,
|
|
258
|
+
error_type=error_type,
|
|
259
|
+
error_message=str(e),
|
|
260
|
+
**{
|
|
261
|
+
k: v for k, v in op_context.items() if k not in ("logger", "start_time")
|
|
262
|
+
},
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Re-raise the exception
|
|
266
|
+
raise
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class ContextTracker:
|
|
270
|
+
"""
|
|
271
|
+
Thread-safe tracker for managing active request contexts.
|
|
272
|
+
|
|
273
|
+
Useful for tracking concurrent requests and their states,
|
|
274
|
+
especially for metrics like active request counts.
|
|
275
|
+
"""
|
|
276
|
+
|
|
277
|
+
def __init__(self) -> None:
|
|
278
|
+
self._active_contexts: dict[str, RequestContext] = {}
|
|
279
|
+
self._lock = asyncio.Lock()
|
|
280
|
+
|
|
281
|
+
async def add_context(self, context: RequestContext) -> None:
|
|
282
|
+
"""Add an active request context."""
|
|
283
|
+
async with self._lock:
|
|
284
|
+
self._active_contexts[context.request_id] = context
|
|
285
|
+
|
|
286
|
+
async def remove_context(self, request_id: str) -> RequestContext | None:
|
|
287
|
+
"""Remove and return a request context."""
|
|
288
|
+
async with self._lock:
|
|
289
|
+
return self._active_contexts.pop(request_id, None)
|
|
290
|
+
|
|
291
|
+
async def get_context(self, request_id: str) -> RequestContext | None:
|
|
292
|
+
"""Get a request context by ID."""
|
|
293
|
+
async with self._lock:
|
|
294
|
+
return self._active_contexts.get(request_id)
|
|
295
|
+
|
|
296
|
+
async def get_active_count(self) -> int:
|
|
297
|
+
"""Get the number of active requests."""
|
|
298
|
+
async with self._lock:
|
|
299
|
+
return len(self._active_contexts)
|
|
300
|
+
|
|
301
|
+
async def get_all_contexts(self) -> dict[str, RequestContext]:
|
|
302
|
+
"""Get a copy of all active contexts."""
|
|
303
|
+
async with self._lock:
|
|
304
|
+
return self._active_contexts.copy()
|
|
305
|
+
|
|
306
|
+
async def cleanup_stale_contexts(self, max_age_seconds: float = 300) -> int:
|
|
307
|
+
"""
|
|
308
|
+
Remove contexts older than max_age_seconds.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
max_age_seconds: Maximum age in seconds before considering stale
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Number of contexts removed
|
|
315
|
+
"""
|
|
316
|
+
current_time = time.perf_counter()
|
|
317
|
+
removed_count = 0
|
|
318
|
+
|
|
319
|
+
async with self._lock:
|
|
320
|
+
stale_ids = [
|
|
321
|
+
request_id
|
|
322
|
+
for request_id, ctx in self._active_contexts.items()
|
|
323
|
+
if (current_time - ctx.start_time) > max_age_seconds
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
for request_id in stale_ids:
|
|
327
|
+
del self._active_contexts[request_id]
|
|
328
|
+
removed_count += 1
|
|
329
|
+
|
|
330
|
+
if removed_count > 0:
|
|
331
|
+
logger.warning(
|
|
332
|
+
"cleanup_stale_contexts",
|
|
333
|
+
removed_count=removed_count,
|
|
334
|
+
max_age_seconds=max_age_seconds,
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return removed_count
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
# Global context tracker instance
|
|
341
|
+
_global_tracker: ContextTracker | None = None
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def get_context_tracker() -> ContextTracker:
|
|
345
|
+
"""Get or create global context tracker."""
|
|
346
|
+
global _global_tracker
|
|
347
|
+
|
|
348
|
+
if _global_tracker is None:
|
|
349
|
+
_global_tracker = ContextTracker()
|
|
350
|
+
|
|
351
|
+
return _global_tracker
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
@asynccontextmanager
|
|
355
|
+
async def tracked_request_context(
|
|
356
|
+
request_id: str | None = None, storage: Any | None = None, **initial_context: Any
|
|
357
|
+
) -> AsyncGenerator[RequestContext, None]:
|
|
358
|
+
"""
|
|
359
|
+
Request context manager that also tracks active requests globally.
|
|
360
|
+
|
|
361
|
+
Combines request_context() with automatic tracking in the global
|
|
362
|
+
context tracker for monitoring active request counts.
|
|
363
|
+
|
|
364
|
+
Args:
|
|
365
|
+
request_id: Unique request identifier
|
|
366
|
+
**initial_context: Initial context to include in log events
|
|
367
|
+
|
|
368
|
+
Yields:
|
|
369
|
+
RequestContext: Context object with timing and logging
|
|
370
|
+
"""
|
|
371
|
+
tracker = get_context_tracker()
|
|
372
|
+
|
|
373
|
+
async with request_context(request_id, storage=storage, **initial_context) as ctx:
|
|
374
|
+
# Add to tracker
|
|
375
|
+
await tracker.add_context(ctx)
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
yield ctx
|
|
379
|
+
finally:
|
|
380
|
+
# Remove from tracker
|
|
381
|
+
await tracker.remove_context(ctx.request_id)
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
async def _emit_request_start_event(
|
|
385
|
+
request_id: str, initial_context: dict[str, Any]
|
|
386
|
+
) -> None:
|
|
387
|
+
"""Emit SSE event for request start."""
|
|
388
|
+
try:
|
|
389
|
+
from ccproxy.observability.sse_events import emit_sse_event
|
|
390
|
+
|
|
391
|
+
# Create event data for SSE
|
|
392
|
+
sse_data = {
|
|
393
|
+
"request_id": request_id,
|
|
394
|
+
"method": initial_context.get("method"),
|
|
395
|
+
"path": initial_context.get("path"),
|
|
396
|
+
"client_ip": initial_context.get("client_ip"),
|
|
397
|
+
"user_agent": initial_context.get("user_agent"),
|
|
398
|
+
"query": initial_context.get("query"),
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
# Remove None values
|
|
402
|
+
sse_data = {k: v for k, v in sse_data.items() if v is not None}
|
|
403
|
+
|
|
404
|
+
await emit_sse_event("request_start", sse_data)
|
|
405
|
+
|
|
406
|
+
except Exception as e:
|
|
407
|
+
# Log error but don't fail the request
|
|
408
|
+
logger.debug(
|
|
409
|
+
"sse_emit_failed",
|
|
410
|
+
event_type="request_start",
|
|
411
|
+
error=str(e),
|
|
412
|
+
request_id=request_id,
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
async def _emit_request_error_event(
|
|
417
|
+
request_id: str, error_type: str, error_message: str, metadata: dict[str, Any]
|
|
418
|
+
) -> None:
|
|
419
|
+
"""Emit SSE event for request error."""
|
|
420
|
+
try:
|
|
421
|
+
from ccproxy.observability.sse_events import emit_sse_event
|
|
422
|
+
|
|
423
|
+
# Create event data for SSE
|
|
424
|
+
sse_data = {
|
|
425
|
+
"request_id": request_id,
|
|
426
|
+
"error_type": error_type,
|
|
427
|
+
"error_message": error_message,
|
|
428
|
+
"method": metadata.get("method"),
|
|
429
|
+
"path": metadata.get("path"),
|
|
430
|
+
"client_ip": metadata.get("client_ip"),
|
|
431
|
+
"user_agent": metadata.get("user_agent"),
|
|
432
|
+
"query": metadata.get("query"),
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
# Remove None values
|
|
436
|
+
sse_data = {k: v for k, v in sse_data.items() if v is not None}
|
|
437
|
+
|
|
438
|
+
await emit_sse_event("request_error", sse_data)
|
|
439
|
+
|
|
440
|
+
except Exception as e:
|
|
441
|
+
# Log error but don't fail the request
|
|
442
|
+
logger.debug(
|
|
443
|
+
"sse_emit_failed",
|
|
444
|
+
event_type="request_error",
|
|
445
|
+
error=str(e),
|
|
446
|
+
request_id=request_id,
|
|
447
|
+
)
|