foundry-mcp 0.8.22__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.
Potentially problematic release.
This version of foundry-mcp might be problematic. Click here for more details.
- foundry_mcp/__init__.py +13 -0
- foundry_mcp/cli/__init__.py +67 -0
- foundry_mcp/cli/__main__.py +9 -0
- foundry_mcp/cli/agent.py +96 -0
- foundry_mcp/cli/commands/__init__.py +37 -0
- foundry_mcp/cli/commands/cache.py +137 -0
- foundry_mcp/cli/commands/dashboard.py +148 -0
- foundry_mcp/cli/commands/dev.py +446 -0
- foundry_mcp/cli/commands/journal.py +377 -0
- foundry_mcp/cli/commands/lifecycle.py +274 -0
- foundry_mcp/cli/commands/modify.py +824 -0
- foundry_mcp/cli/commands/plan.py +640 -0
- foundry_mcp/cli/commands/pr.py +393 -0
- foundry_mcp/cli/commands/review.py +667 -0
- foundry_mcp/cli/commands/session.py +472 -0
- foundry_mcp/cli/commands/specs.py +686 -0
- foundry_mcp/cli/commands/tasks.py +807 -0
- foundry_mcp/cli/commands/testing.py +676 -0
- foundry_mcp/cli/commands/validate.py +982 -0
- foundry_mcp/cli/config.py +98 -0
- foundry_mcp/cli/context.py +298 -0
- foundry_mcp/cli/logging.py +212 -0
- foundry_mcp/cli/main.py +44 -0
- foundry_mcp/cli/output.py +122 -0
- foundry_mcp/cli/registry.py +110 -0
- foundry_mcp/cli/resilience.py +178 -0
- foundry_mcp/cli/transcript.py +217 -0
- foundry_mcp/config.py +1454 -0
- foundry_mcp/core/__init__.py +144 -0
- foundry_mcp/core/ai_consultation.py +1773 -0
- foundry_mcp/core/batch_operations.py +1202 -0
- foundry_mcp/core/cache.py +195 -0
- foundry_mcp/core/capabilities.py +446 -0
- foundry_mcp/core/concurrency.py +898 -0
- foundry_mcp/core/context.py +540 -0
- foundry_mcp/core/discovery.py +1603 -0
- foundry_mcp/core/error_collection.py +728 -0
- foundry_mcp/core/error_store.py +592 -0
- foundry_mcp/core/health.py +749 -0
- foundry_mcp/core/intake.py +933 -0
- foundry_mcp/core/journal.py +700 -0
- foundry_mcp/core/lifecycle.py +412 -0
- foundry_mcp/core/llm_config.py +1376 -0
- foundry_mcp/core/llm_patterns.py +510 -0
- foundry_mcp/core/llm_provider.py +1569 -0
- foundry_mcp/core/logging_config.py +374 -0
- foundry_mcp/core/metrics_persistence.py +584 -0
- foundry_mcp/core/metrics_registry.py +327 -0
- foundry_mcp/core/metrics_store.py +641 -0
- foundry_mcp/core/modifications.py +224 -0
- foundry_mcp/core/naming.py +146 -0
- foundry_mcp/core/observability.py +1216 -0
- foundry_mcp/core/otel.py +452 -0
- foundry_mcp/core/otel_stubs.py +264 -0
- foundry_mcp/core/pagination.py +255 -0
- foundry_mcp/core/progress.py +387 -0
- foundry_mcp/core/prometheus.py +564 -0
- foundry_mcp/core/prompts/__init__.py +464 -0
- foundry_mcp/core/prompts/fidelity_review.py +691 -0
- foundry_mcp/core/prompts/markdown_plan_review.py +515 -0
- foundry_mcp/core/prompts/plan_review.py +627 -0
- foundry_mcp/core/providers/__init__.py +237 -0
- foundry_mcp/core/providers/base.py +515 -0
- foundry_mcp/core/providers/claude.py +472 -0
- foundry_mcp/core/providers/codex.py +637 -0
- foundry_mcp/core/providers/cursor_agent.py +630 -0
- foundry_mcp/core/providers/detectors.py +515 -0
- foundry_mcp/core/providers/gemini.py +426 -0
- foundry_mcp/core/providers/opencode.py +718 -0
- foundry_mcp/core/providers/opencode_wrapper.js +308 -0
- foundry_mcp/core/providers/package-lock.json +24 -0
- foundry_mcp/core/providers/package.json +25 -0
- foundry_mcp/core/providers/registry.py +607 -0
- foundry_mcp/core/providers/test_provider.py +171 -0
- foundry_mcp/core/providers/validation.py +857 -0
- foundry_mcp/core/rate_limit.py +427 -0
- foundry_mcp/core/research/__init__.py +68 -0
- foundry_mcp/core/research/memory.py +528 -0
- foundry_mcp/core/research/models.py +1234 -0
- foundry_mcp/core/research/providers/__init__.py +40 -0
- foundry_mcp/core/research/providers/base.py +242 -0
- foundry_mcp/core/research/providers/google.py +507 -0
- foundry_mcp/core/research/providers/perplexity.py +442 -0
- foundry_mcp/core/research/providers/semantic_scholar.py +544 -0
- foundry_mcp/core/research/providers/tavily.py +383 -0
- foundry_mcp/core/research/workflows/__init__.py +25 -0
- foundry_mcp/core/research/workflows/base.py +298 -0
- foundry_mcp/core/research/workflows/chat.py +271 -0
- foundry_mcp/core/research/workflows/consensus.py +539 -0
- foundry_mcp/core/research/workflows/deep_research.py +4142 -0
- foundry_mcp/core/research/workflows/ideate.py +682 -0
- foundry_mcp/core/research/workflows/thinkdeep.py +405 -0
- foundry_mcp/core/resilience.py +600 -0
- foundry_mcp/core/responses.py +1624 -0
- foundry_mcp/core/review.py +366 -0
- foundry_mcp/core/security.py +438 -0
- foundry_mcp/core/spec.py +4119 -0
- foundry_mcp/core/task.py +2463 -0
- foundry_mcp/core/testing.py +839 -0
- foundry_mcp/core/validation.py +2357 -0
- foundry_mcp/dashboard/__init__.py +32 -0
- foundry_mcp/dashboard/app.py +119 -0
- foundry_mcp/dashboard/components/__init__.py +17 -0
- foundry_mcp/dashboard/components/cards.py +88 -0
- foundry_mcp/dashboard/components/charts.py +177 -0
- foundry_mcp/dashboard/components/filters.py +136 -0
- foundry_mcp/dashboard/components/tables.py +195 -0
- foundry_mcp/dashboard/data/__init__.py +11 -0
- foundry_mcp/dashboard/data/stores.py +433 -0
- foundry_mcp/dashboard/launcher.py +300 -0
- foundry_mcp/dashboard/views/__init__.py +12 -0
- foundry_mcp/dashboard/views/errors.py +217 -0
- foundry_mcp/dashboard/views/metrics.py +164 -0
- foundry_mcp/dashboard/views/overview.py +96 -0
- foundry_mcp/dashboard/views/providers.py +83 -0
- foundry_mcp/dashboard/views/sdd_workflow.py +255 -0
- foundry_mcp/dashboard/views/tool_usage.py +139 -0
- foundry_mcp/prompts/__init__.py +9 -0
- foundry_mcp/prompts/workflows.py +525 -0
- foundry_mcp/resources/__init__.py +9 -0
- foundry_mcp/resources/specs.py +591 -0
- foundry_mcp/schemas/__init__.py +38 -0
- foundry_mcp/schemas/intake-schema.json +89 -0
- foundry_mcp/schemas/sdd-spec-schema.json +414 -0
- foundry_mcp/server.py +150 -0
- foundry_mcp/tools/__init__.py +10 -0
- foundry_mcp/tools/unified/__init__.py +92 -0
- foundry_mcp/tools/unified/authoring.py +3620 -0
- foundry_mcp/tools/unified/context_helpers.py +98 -0
- foundry_mcp/tools/unified/documentation_helpers.py +268 -0
- foundry_mcp/tools/unified/environment.py +1341 -0
- foundry_mcp/tools/unified/error.py +479 -0
- foundry_mcp/tools/unified/health.py +225 -0
- foundry_mcp/tools/unified/journal.py +841 -0
- foundry_mcp/tools/unified/lifecycle.py +640 -0
- foundry_mcp/tools/unified/metrics.py +777 -0
- foundry_mcp/tools/unified/plan.py +876 -0
- foundry_mcp/tools/unified/pr.py +294 -0
- foundry_mcp/tools/unified/provider.py +589 -0
- foundry_mcp/tools/unified/research.py +1283 -0
- foundry_mcp/tools/unified/review.py +1042 -0
- foundry_mcp/tools/unified/review_helpers.py +314 -0
- foundry_mcp/tools/unified/router.py +102 -0
- foundry_mcp/tools/unified/server.py +565 -0
- foundry_mcp/tools/unified/spec.py +1283 -0
- foundry_mcp/tools/unified/task.py +3846 -0
- foundry_mcp/tools/unified/test.py +431 -0
- foundry_mcp/tools/unified/verification.py +520 -0
- foundry_mcp-0.8.22.dist-info/METADATA +344 -0
- foundry_mcp-0.8.22.dist-info/RECORD +153 -0
- foundry_mcp-0.8.22.dist-info/WHEEL +4 -0
- foundry_mcp-0.8.22.dist-info/entry_points.txt +3 -0
- foundry_mcp-0.8.22.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
"""Unified context management for request correlation and distributed tracing.
|
|
2
|
+
|
|
3
|
+
This module provides a single source of truth for request context propagation
|
|
4
|
+
across the foundry-mcp codebase, including:
|
|
5
|
+
|
|
6
|
+
- Correlation ID generation and propagation
|
|
7
|
+
- W3C Trace Context (traceparent/tracestate) support
|
|
8
|
+
- Thread-safe context variables via contextvars
|
|
9
|
+
- Both async and sync context managers
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from foundry_mcp.core.context import (
|
|
13
|
+
request_context,
|
|
14
|
+
sync_request_context,
|
|
15
|
+
get_correlation_id,
|
|
16
|
+
get_current_context,
|
|
17
|
+
generate_correlation_id,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Async usage
|
|
21
|
+
async with request_context(client_id="user123") as ctx:
|
|
22
|
+
print(ctx.correlation_id) # e.g., "req_a1b2c3d4e5f6"
|
|
23
|
+
|
|
24
|
+
# Sync usage
|
|
25
|
+
with sync_request_context() as ctx:
|
|
26
|
+
print(ctx.correlation_id)
|
|
27
|
+
|
|
28
|
+
# Manual ID generation
|
|
29
|
+
corr_id = generate_correlation_id(prefix="task") # "task_a1b2c3d4e5f6"
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import re
|
|
35
|
+
import secrets
|
|
36
|
+
import time
|
|
37
|
+
from contextlib import contextmanager
|
|
38
|
+
from contextvars import ContextVar
|
|
39
|
+
from dataclasses import dataclass, field
|
|
40
|
+
from typing import Any, Dict, Generator, Optional
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
# Context variables (for advanced use)
|
|
44
|
+
"correlation_id_var",
|
|
45
|
+
"client_id_var",
|
|
46
|
+
"start_time_var",
|
|
47
|
+
"trace_context_var",
|
|
48
|
+
# Dataclasses
|
|
49
|
+
"W3CTraceContext",
|
|
50
|
+
"RequestContext",
|
|
51
|
+
# ID generation
|
|
52
|
+
"generate_correlation_id",
|
|
53
|
+
# Context managers
|
|
54
|
+
"request_context",
|
|
55
|
+
"sync_request_context",
|
|
56
|
+
# Accessors
|
|
57
|
+
"get_correlation_id",
|
|
58
|
+
"get_client_id",
|
|
59
|
+
"get_start_time",
|
|
60
|
+
"get_trace_context",
|
|
61
|
+
"get_current_context",
|
|
62
|
+
# W3C helpers
|
|
63
|
+
"parse_traceparent",
|
|
64
|
+
"format_traceparent",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
# -----------------------------------------------------------------------------
|
|
68
|
+
# Context Variables
|
|
69
|
+
# -----------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
correlation_id_var: ContextVar[str] = ContextVar("correlation_id", default="")
|
|
72
|
+
"""Request correlation ID for tracing requests across components."""
|
|
73
|
+
|
|
74
|
+
client_id_var: ContextVar[str] = ContextVar("client_id", default="anonymous")
|
|
75
|
+
"""Identifier for the client making the request."""
|
|
76
|
+
|
|
77
|
+
start_time_var: ContextVar[float] = ContextVar("start_time", default=0.0)
|
|
78
|
+
"""Request start time as Unix timestamp."""
|
|
79
|
+
|
|
80
|
+
trace_context_var: ContextVar[Optional["W3CTraceContext"]] = ContextVar(
|
|
81
|
+
"trace_context", default=None
|
|
82
|
+
)
|
|
83
|
+
"""W3C Trace Context for distributed tracing integration."""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# -----------------------------------------------------------------------------
|
|
87
|
+
# W3C Trace Context Support
|
|
88
|
+
# -----------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
# W3C traceparent format: version-trace_id-parent_id-flags
|
|
91
|
+
# Example: 00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01
|
|
92
|
+
_TRACEPARENT_REGEX = re.compile(
|
|
93
|
+
r"^(?P<version>[0-9a-f]{2})-"
|
|
94
|
+
r"(?P<trace_id>[0-9a-f]{32})-"
|
|
95
|
+
r"(?P<parent_id>[0-9a-f]{16})-"
|
|
96
|
+
r"(?P<flags>[0-9a-f]{2})$"
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class W3CTraceContext:
|
|
102
|
+
"""W3C Trace Context representation for distributed tracing.
|
|
103
|
+
|
|
104
|
+
Implements the W3C Trace Context specification:
|
|
105
|
+
https://www.w3.org/TR/trace-context/
|
|
106
|
+
|
|
107
|
+
Attributes:
|
|
108
|
+
version: Trace context version (currently "00")
|
|
109
|
+
trace_id: 32-character hex trace identifier
|
|
110
|
+
parent_id: 16-character hex span/parent identifier
|
|
111
|
+
flags: 2-character hex flags (01 = sampled)
|
|
112
|
+
tracestate: Optional vendor-specific trace state
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
version: str = "00"
|
|
116
|
+
trace_id: str = ""
|
|
117
|
+
parent_id: str = ""
|
|
118
|
+
flags: str = "00"
|
|
119
|
+
tracestate: Optional[str] = None
|
|
120
|
+
|
|
121
|
+
@classmethod
|
|
122
|
+
def parse(
|
|
123
|
+
cls,
|
|
124
|
+
traceparent: Optional[str] = None,
|
|
125
|
+
tracestate: Optional[str] = None,
|
|
126
|
+
) -> Optional["W3CTraceContext"]:
|
|
127
|
+
"""Parse W3C traceparent and tracestate headers.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
traceparent: The traceparent header value
|
|
131
|
+
tracestate: Optional tracestate header value
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Parsed W3CTraceContext or None if parsing fails
|
|
135
|
+
"""
|
|
136
|
+
if not traceparent:
|
|
137
|
+
return None
|
|
138
|
+
|
|
139
|
+
match = _TRACEPARENT_REGEX.match(traceparent.lower().strip())
|
|
140
|
+
if not match:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
return cls(
|
|
144
|
+
version=match.group("version"),
|
|
145
|
+
trace_id=match.group("trace_id"),
|
|
146
|
+
parent_id=match.group("parent_id"),
|
|
147
|
+
flags=match.group("flags"),
|
|
148
|
+
tracestate=tracestate,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
@classmethod
|
|
152
|
+
def generate(cls, sampled: bool = True) -> "W3CTraceContext":
|
|
153
|
+
"""Generate a new W3C Trace Context.
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
sampled: Whether this trace should be sampled
|
|
157
|
+
|
|
158
|
+
Returns:
|
|
159
|
+
New W3CTraceContext with generated IDs
|
|
160
|
+
"""
|
|
161
|
+
return cls(
|
|
162
|
+
version="00",
|
|
163
|
+
trace_id=secrets.token_hex(16), # 32 hex chars
|
|
164
|
+
parent_id=secrets.token_hex(8), # 16 hex chars
|
|
165
|
+
flags="01" if sampled else "00",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
def is_sampled(self) -> bool:
|
|
170
|
+
"""Check if trace is sampled based on flags."""
|
|
171
|
+
try:
|
|
172
|
+
return (int(self.flags, 16) & 0x01) == 0x01
|
|
173
|
+
except ValueError:
|
|
174
|
+
return False
|
|
175
|
+
|
|
176
|
+
@property
|
|
177
|
+
def traceparent(self) -> str:
|
|
178
|
+
"""Format as W3C traceparent header value."""
|
|
179
|
+
return f"{self.version}-{self.trace_id}-{self.parent_id}-{self.flags}"
|
|
180
|
+
|
|
181
|
+
def with_new_parent(self, sampled: Optional[bool] = None) -> "W3CTraceContext":
|
|
182
|
+
"""Create child context with new parent_id, preserving trace_id.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
sampled: Override sampling decision (None = inherit)
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
New W3CTraceContext with same trace_id but new parent_id
|
|
189
|
+
"""
|
|
190
|
+
flags = self.flags
|
|
191
|
+
if sampled is not None:
|
|
192
|
+
flags = "01" if sampled else "00"
|
|
193
|
+
|
|
194
|
+
return W3CTraceContext(
|
|
195
|
+
version=self.version,
|
|
196
|
+
trace_id=self.trace_id,
|
|
197
|
+
parent_id=secrets.token_hex(8),
|
|
198
|
+
flags=flags,
|
|
199
|
+
tracestate=self.tracestate,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
203
|
+
"""Convert to dictionary for serialization."""
|
|
204
|
+
result = {
|
|
205
|
+
"version": self.version,
|
|
206
|
+
"trace_id": self.trace_id,
|
|
207
|
+
"parent_id": self.parent_id,
|
|
208
|
+
"flags": self.flags,
|
|
209
|
+
"sampled": self.is_sampled,
|
|
210
|
+
}
|
|
211
|
+
if self.tracestate:
|
|
212
|
+
result["tracestate"] = self.tracestate
|
|
213
|
+
return result
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def parse_traceparent(header: Optional[str]) -> Optional[W3CTraceContext]:
|
|
217
|
+
"""Parse a traceparent header into W3CTraceContext.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
header: The traceparent header value
|
|
221
|
+
|
|
222
|
+
Returns:
|
|
223
|
+
Parsed context or None
|
|
224
|
+
"""
|
|
225
|
+
return W3CTraceContext.parse(header)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def format_traceparent(ctx: W3CTraceContext) -> str:
|
|
229
|
+
"""Format W3CTraceContext as traceparent header.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
ctx: The trace context
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Formatted traceparent header value
|
|
236
|
+
"""
|
|
237
|
+
return ctx.traceparent
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
# -----------------------------------------------------------------------------
|
|
241
|
+
# Correlation ID Generation
|
|
242
|
+
# -----------------------------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def generate_correlation_id(prefix: str = "req") -> str:
|
|
246
|
+
"""Generate a unique correlation ID with optional prefix.
|
|
247
|
+
|
|
248
|
+
Format: {prefix}_{12_hex_chars}
|
|
249
|
+
Example: "req_a1b2c3d4e5f6"
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
prefix: ID prefix (default: "req")
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Unique correlation ID string
|
|
256
|
+
"""
|
|
257
|
+
return f"{prefix}_{secrets.token_hex(6)}"
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
# -----------------------------------------------------------------------------
|
|
261
|
+
# Request Context
|
|
262
|
+
# -----------------------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
@dataclass
|
|
266
|
+
class RequestContext:
|
|
267
|
+
"""Immutable snapshot of the current request context.
|
|
268
|
+
|
|
269
|
+
This dataclass captures all context variables at a point in time,
|
|
270
|
+
providing convenient access to timing information and serialization.
|
|
271
|
+
|
|
272
|
+
Attributes:
|
|
273
|
+
correlation_id: Unique request identifier
|
|
274
|
+
client_id: Client/user identifier
|
|
275
|
+
start_time: Request start timestamp
|
|
276
|
+
trace_context: Optional W3C distributed tracing context
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
correlation_id: str = ""
|
|
280
|
+
client_id: str = "anonymous"
|
|
281
|
+
start_time: float = field(default_factory=time.time)
|
|
282
|
+
trace_context: Optional[W3CTraceContext] = None
|
|
283
|
+
|
|
284
|
+
@property
|
|
285
|
+
def elapsed_seconds(self) -> float:
|
|
286
|
+
"""Calculate elapsed time since request start in seconds."""
|
|
287
|
+
if self.start_time <= 0:
|
|
288
|
+
return 0.0
|
|
289
|
+
return time.time() - self.start_time
|
|
290
|
+
|
|
291
|
+
@property
|
|
292
|
+
def elapsed_ms(self) -> float:
|
|
293
|
+
"""Calculate elapsed time since request start in milliseconds."""
|
|
294
|
+
return self.elapsed_seconds * 1000
|
|
295
|
+
|
|
296
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
297
|
+
"""Convert context to dictionary for logging/serialization.
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Dictionary with all context fields
|
|
301
|
+
"""
|
|
302
|
+
result: Dict[str, Any] = {
|
|
303
|
+
"correlation_id": self.correlation_id,
|
|
304
|
+
"client_id": self.client_id,
|
|
305
|
+
"start_time": self.start_time,
|
|
306
|
+
"elapsed_ms": round(self.elapsed_ms, 2),
|
|
307
|
+
}
|
|
308
|
+
if self.trace_context:
|
|
309
|
+
result["trace"] = self.trace_context.to_dict()
|
|
310
|
+
return result
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
# -----------------------------------------------------------------------------
|
|
314
|
+
# Context Managers
|
|
315
|
+
# -----------------------------------------------------------------------------
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
@contextmanager
|
|
319
|
+
def sync_request_context(
|
|
320
|
+
*,
|
|
321
|
+
correlation_id: Optional[str] = None,
|
|
322
|
+
client_id: Optional[str] = None,
|
|
323
|
+
traceparent: Optional[str] = None,
|
|
324
|
+
tracestate: Optional[str] = None,
|
|
325
|
+
) -> Generator[RequestContext, None, None]:
|
|
326
|
+
"""Synchronous context manager for request context.
|
|
327
|
+
|
|
328
|
+
Sets up context variables for the duration of the with block,
|
|
329
|
+
automatically cleaning up on exit.
|
|
330
|
+
|
|
331
|
+
Args:
|
|
332
|
+
correlation_id: Request ID (auto-generated if None)
|
|
333
|
+
client_id: Client identifier (default: "anonymous")
|
|
334
|
+
traceparent: W3C traceparent header for distributed tracing
|
|
335
|
+
tracestate: W3C tracestate header
|
|
336
|
+
|
|
337
|
+
Yields:
|
|
338
|
+
RequestContext snapshot
|
|
339
|
+
|
|
340
|
+
Example:
|
|
341
|
+
with sync_request_context(client_id="user123") as ctx:
|
|
342
|
+
logger.info(f"Processing request {ctx.correlation_id}")
|
|
343
|
+
"""
|
|
344
|
+
# Generate or use provided correlation ID
|
|
345
|
+
corr_id = correlation_id or generate_correlation_id()
|
|
346
|
+
client = client_id or "anonymous"
|
|
347
|
+
start = time.time()
|
|
348
|
+
|
|
349
|
+
# Parse trace context if provided
|
|
350
|
+
trace_ctx = W3CTraceContext.parse(traceparent, tracestate)
|
|
351
|
+
|
|
352
|
+
# Set context variables
|
|
353
|
+
token_corr = correlation_id_var.set(corr_id)
|
|
354
|
+
token_client = client_id_var.set(client)
|
|
355
|
+
token_start = start_time_var.set(start)
|
|
356
|
+
token_trace = trace_context_var.set(trace_ctx)
|
|
357
|
+
|
|
358
|
+
try:
|
|
359
|
+
yield RequestContext(
|
|
360
|
+
correlation_id=corr_id,
|
|
361
|
+
client_id=client,
|
|
362
|
+
start_time=start,
|
|
363
|
+
trace_context=trace_ctx,
|
|
364
|
+
)
|
|
365
|
+
finally:
|
|
366
|
+
# Reset context variables
|
|
367
|
+
correlation_id_var.reset(token_corr)
|
|
368
|
+
client_id_var.reset(token_client)
|
|
369
|
+
start_time_var.reset(token_start)
|
|
370
|
+
trace_context_var.reset(token_trace)
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
async def request_context(
|
|
374
|
+
*,
|
|
375
|
+
correlation_id: Optional[str] = None,
|
|
376
|
+
client_id: Optional[str] = None,
|
|
377
|
+
traceparent: Optional[str] = None,
|
|
378
|
+
tracestate: Optional[str] = None,
|
|
379
|
+
) -> RequestContext:
|
|
380
|
+
"""Async context manager for request context.
|
|
381
|
+
|
|
382
|
+
This is an async generator that can be used with `async with`.
|
|
383
|
+
Sets up context variables for the duration of the async with block.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
correlation_id: Request ID (auto-generated if None)
|
|
387
|
+
client_id: Client identifier (default: "anonymous")
|
|
388
|
+
traceparent: W3C traceparent header for distributed tracing
|
|
389
|
+
tracestate: W3C tracestate header
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
RequestContext snapshot (use with async context manager)
|
|
393
|
+
|
|
394
|
+
Example:
|
|
395
|
+
async with request_context(client_id="user123") as ctx:
|
|
396
|
+
logger.info(f"Processing request {ctx.correlation_id}")
|
|
397
|
+
"""
|
|
398
|
+
# This uses the sync implementation since contextvars work across
|
|
399
|
+
# sync/async boundaries in Python 3.7+
|
|
400
|
+
with sync_request_context(
|
|
401
|
+
correlation_id=correlation_id,
|
|
402
|
+
client_id=client_id,
|
|
403
|
+
traceparent=traceparent,
|
|
404
|
+
tracestate=tracestate,
|
|
405
|
+
) as ctx:
|
|
406
|
+
return ctx
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
# Make request_context work as both async and sync context manager
|
|
410
|
+
class _AsyncContextManager:
|
|
411
|
+
"""Wrapper to make request_context work as async context manager."""
|
|
412
|
+
|
|
413
|
+
def __init__(
|
|
414
|
+
self,
|
|
415
|
+
correlation_id: Optional[str] = None,
|
|
416
|
+
client_id: Optional[str] = None,
|
|
417
|
+
traceparent: Optional[str] = None,
|
|
418
|
+
tracestate: Optional[str] = None,
|
|
419
|
+
):
|
|
420
|
+
self.correlation_id = correlation_id
|
|
421
|
+
self.client_id = client_id
|
|
422
|
+
self.traceparent = traceparent
|
|
423
|
+
self.tracestate = tracestate
|
|
424
|
+
self._sync_cm: Optional[Generator[RequestContext, None, None]] = None
|
|
425
|
+
self._ctx: Optional[RequestContext] = None
|
|
426
|
+
|
|
427
|
+
async def __aenter__(self) -> RequestContext:
|
|
428
|
+
self._sync_cm = sync_request_context(
|
|
429
|
+
correlation_id=self.correlation_id,
|
|
430
|
+
client_id=self.client_id,
|
|
431
|
+
traceparent=self.traceparent,
|
|
432
|
+
tracestate=self.tracestate,
|
|
433
|
+
)
|
|
434
|
+
self._ctx = self._sync_cm.__enter__()
|
|
435
|
+
return self._ctx
|
|
436
|
+
|
|
437
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
438
|
+
if self._sync_cm:
|
|
439
|
+
self._sync_cm.__exit__(exc_type, exc_val, exc_tb)
|
|
440
|
+
return False
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def async_request_context(
|
|
444
|
+
*,
|
|
445
|
+
correlation_id: Optional[str] = None,
|
|
446
|
+
client_id: Optional[str] = None,
|
|
447
|
+
traceparent: Optional[str] = None,
|
|
448
|
+
tracestate: Optional[str] = None,
|
|
449
|
+
) -> _AsyncContextManager:
|
|
450
|
+
"""Create an async context manager for request context.
|
|
451
|
+
|
|
452
|
+
Args:
|
|
453
|
+
correlation_id: Request ID (auto-generated if None)
|
|
454
|
+
client_id: Client identifier (default: "anonymous")
|
|
455
|
+
traceparent: W3C traceparent header for distributed tracing
|
|
456
|
+
tracestate: W3C tracestate header
|
|
457
|
+
|
|
458
|
+
Returns:
|
|
459
|
+
Async context manager yielding RequestContext
|
|
460
|
+
|
|
461
|
+
Example:
|
|
462
|
+
async with async_request_context(client_id="user123") as ctx:
|
|
463
|
+
await some_async_operation()
|
|
464
|
+
logger.info(f"Completed request {ctx.correlation_id}")
|
|
465
|
+
"""
|
|
466
|
+
return _AsyncContextManager(
|
|
467
|
+
correlation_id=correlation_id,
|
|
468
|
+
client_id=client_id,
|
|
469
|
+
traceparent=traceparent,
|
|
470
|
+
tracestate=tracestate,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# -----------------------------------------------------------------------------
|
|
475
|
+
# Context Accessors
|
|
476
|
+
# -----------------------------------------------------------------------------
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
def get_correlation_id() -> str:
|
|
480
|
+
"""Get the current correlation ID from context.
|
|
481
|
+
|
|
482
|
+
Returns:
|
|
483
|
+
Current correlation ID or empty string if not set
|
|
484
|
+
"""
|
|
485
|
+
return correlation_id_var.get()
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def get_client_id() -> str:
|
|
489
|
+
"""Get the current client ID from context.
|
|
490
|
+
|
|
491
|
+
Returns:
|
|
492
|
+
Current client ID or "anonymous" if not set
|
|
493
|
+
"""
|
|
494
|
+
return client_id_var.get()
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
def get_start_time() -> float:
|
|
498
|
+
"""Get the request start time from context.
|
|
499
|
+
|
|
500
|
+
Returns:
|
|
501
|
+
Start time as Unix timestamp or 0.0 if not set
|
|
502
|
+
"""
|
|
503
|
+
return start_time_var.get()
|
|
504
|
+
|
|
505
|
+
|
|
506
|
+
def get_trace_context() -> Optional[W3CTraceContext]:
|
|
507
|
+
"""Get the W3C trace context if set.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
Current W3CTraceContext or None
|
|
511
|
+
"""
|
|
512
|
+
return trace_context_var.get()
|
|
513
|
+
|
|
514
|
+
|
|
515
|
+
def get_current_context() -> RequestContext:
|
|
516
|
+
"""Get a snapshot of all current context values.
|
|
517
|
+
|
|
518
|
+
Returns:
|
|
519
|
+
RequestContext with current values from context variables
|
|
520
|
+
"""
|
|
521
|
+
return RequestContext(
|
|
522
|
+
correlation_id=get_correlation_id(),
|
|
523
|
+
client_id=get_client_id(),
|
|
524
|
+
start_time=get_start_time(),
|
|
525
|
+
trace_context=get_trace_context(),
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
# -----------------------------------------------------------------------------
|
|
530
|
+
# Backward Compatibility Aliases
|
|
531
|
+
# -----------------------------------------------------------------------------
|
|
532
|
+
|
|
533
|
+
# For backward compatibility with concurrency.py
|
|
534
|
+
request_id = correlation_id_var
|
|
535
|
+
"""Alias for correlation_id_var (backward compatibility)."""
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def get_request_id() -> str:
|
|
539
|
+
"""Alias for get_correlation_id() (backward compatibility)."""
|
|
540
|
+
return get_correlation_id()
|