Skip to content

Commit 08afc47

Browse files
refactor wsgi + redis
1 parent 00dcd88 commit 08afc47

5 files changed

Lines changed: 520 additions & 167 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,4 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
3939

4040
## License
4141

42-
MIT
42+
Apache License 2.0

drift/core/mode_utils.py

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""Mode utilities for handling RECORD and REPLAY mode logic.
2+
3+
This module provides utilities that abstract common mode-handling patterns,
4+
matching the Node SDK's modeUtils.ts. These utilities help instrumentations
5+
decide how to handle requests based on the SDK mode and app state.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import logging
11+
from typing import TYPE_CHECKING, Callable, TypeVar
12+
13+
from opentelemetry.trace import SpanKind as OTelSpanKind
14+
15+
if TYPE_CHECKING:
16+
from .tracing.span_utils import SpanInfo
17+
18+
logger = logging.getLogger(__name__)
19+
20+
T = TypeVar("T")
21+
22+
# Type aliases for handler functions
23+
OriginalFunctionCall = Callable[[], T]
24+
RecordModeHandler = Callable[[bool], T] # (is_pre_app_start: bool) -> T
25+
ReplayModeHandler = Callable[[], T]
26+
NoOpRequestHandler = Callable[[], T]
27+
28+
29+
def handle_record_mode(
30+
original_function_call: OriginalFunctionCall[T],
31+
record_mode_handler: RecordModeHandler[T],
32+
span_kind: OTelSpanKind,
33+
) -> T:
34+
"""Handle RECORD mode logic for instrumentations.
35+
36+
This utility abstracts the common record mode pattern of checking for
37+
current span context and deciding whether to execute record mode logic
38+
or just call the original function.
39+
40+
Decision logic:
41+
- If app NOT ready -> call record_mode_handler(is_pre_app_start=True)
42+
- If no span context AND not SERVER span, OR span was pre-app-start -> call original_function_call() (skip)
43+
- Otherwise -> call record_mode_handler(is_pre_app_start=False)
44+
45+
Args:
46+
original_function_call: Function that calls the original function when no span context exists
47+
record_mode_handler: Function that handles record mode logic; receives is_pre_app_start flag
48+
span_kind: The kind of span being created (determines if this is a server request)
49+
50+
Returns:
51+
Result from either original_function_call or record_mode_handler
52+
"""
53+
from .drift_sdk import TuskDrift
54+
from .tracing.span_utils import SpanUtils
55+
56+
try:
57+
sdk = TuskDrift.get_instance()
58+
is_app_ready = sdk.is_app_ready()
59+
current_span_info = SpanUtils.get_current_span_info()
60+
except Exception as e:
61+
logger.error(f"ModeUtils error checking app readiness or getting current span info: {e}")
62+
return original_function_call()
63+
64+
if not is_app_ready:
65+
# App not ready - record with is_pre_app_start=True
66+
return record_mode_handler(True)
67+
68+
# App is ready - check span context
69+
is_server_span = span_kind == OTelSpanKind.SERVER
70+
71+
if (not current_span_info and not is_server_span) or (
72+
current_span_info and current_span_info.is_pre_app_start
73+
):
74+
# No span context and not a server request, OR within a pre-app-start span
75+
# Skip recording - call original function
76+
return original_function_call()
77+
78+
# App ready with valid span context - record with is_pre_app_start=False
79+
return record_mode_handler(False)
80+
81+
82+
def handle_replay_mode(
83+
replay_mode_handler: ReplayModeHandler[T],
84+
no_op_request_handler: NoOpRequestHandler[T],
85+
is_server_request: bool,
86+
) -> T:
87+
"""Handle REPLAY mode logic for instrumentations.
88+
89+
This utility abstracts the common replay mode pattern of checking if
90+
the request is a background request.
91+
92+
Decision logic:
93+
- If background request (app ready + no parent span + not server request) -> call no_op_request_handler()
94+
- Otherwise -> call replay_mode_handler()
95+
96+
Background requests are requests that happen after app startup but outside
97+
of any trace context (health checks, background jobs, etc.). In REPLAY mode,
98+
these should return dummy responses instead of querying for mocks.
99+
100+
Args:
101+
replay_mode_handler: Function that handles normal replay mode logic (fetches mocks)
102+
no_op_request_handler: Function that returns a dummy/no-op response for background requests
103+
is_server_request: True if this is a SERVER span (inbound HTTP request)
104+
105+
Returns:
106+
Result from either no_op_request_handler or replay_mode_handler
107+
"""
108+
from .drift_sdk import TuskDrift
109+
from .tracing.span_utils import SpanUtils
110+
111+
sdk = TuskDrift.get_instance()
112+
is_app_ready = sdk.is_app_ready()
113+
current_span_info = SpanUtils.get_current_span_info()
114+
115+
# Background request: App is ready + not within a trace (no parent span) + not a server request
116+
if is_app_ready and not current_span_info and not is_server_request:
117+
logger.debug("[ModeUtils] Handling no-op request")
118+
return no_op_request_handler()
119+
120+
return replay_mode_handler()
121+
122+
123+
def is_background_request(is_server_request: bool = False) -> bool:
124+
"""Check if the current request is a background request.
125+
126+
A background request is one that:
127+
- Happens after app is ready (not pre-app-start)
128+
- Has no parent span context (not within an existing trace)
129+
- Is not a server request (not an incoming HTTP request that starts a new trace)
130+
131+
Background requests should typically be handled with no-op/dummy responses
132+
in REPLAY mode since they were never recorded.
133+
134+
Args:
135+
is_server_request: True if this is a SERVER span type
136+
137+
Returns:
138+
True if this is a background request, False otherwise
139+
"""
140+
from .drift_sdk import TuskDrift
141+
from .tracing.span_utils import SpanUtils
142+
143+
sdk = TuskDrift.get_instance()
144+
is_app_ready = sdk.is_app_ready()
145+
current_span_info = SpanUtils.get_current_span_info()
146+
147+
return is_app_ready and not current_span_info and not is_server_request

drift/core/tracing/span_utils.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ class SpanInfo:
4141

4242
trace_id: str
4343
span_id: str
44+
parent_span_id: str | None
4445
span: Span
4546
context: Context
4647
is_pre_app_start: bool
@@ -146,10 +147,14 @@ def create_span(options: CreateSpanOptions) -> SpanInfo | None:
146147
# Check if we should block span creation for this trace
147148
# (This matches the trace blocking check in Node.js SDK)
148149
active_span = trace.get_current_span(parent_context)
150+
parent_span_id: str | None = None
151+
149152
if active_span and active_span.is_recording():
150153
from ..trace_blocking_manager import TraceBlockingManager
151154

152-
parent_trace_id = format_trace_id(active_span.get_span_context().trace_id)
155+
parent_span_context = active_span.get_span_context()
156+
parent_trace_id = format_trace_id(parent_span_context.trace_id)
157+
parent_span_id = format_span_id(parent_span_context.span_id)
153158
trace_blocking_manager = TraceBlockingManager.get_instance()
154159

155160
if trace_blocking_manager.is_trace_blocked(parent_trace_id):
@@ -182,6 +187,7 @@ def create_span(options: CreateSpanOptions) -> SpanInfo | None:
182187
return SpanInfo(
183188
trace_id=trace_id,
184189
span_id=span_id,
190+
parent_span_id=parent_span_id,
185191
span=span,
186192
context=new_context,
187193
is_pre_app_start=options.is_pre_app_start,
@@ -319,6 +325,10 @@ def get_current_span_info() -> SpanInfo | None:
319325
trace_id = format_trace_id(span_context.trace_id)
320326
span_id = format_span_id(span_context.span_id)
321327

328+
# Note: We can't easily get the parent span ID from an already-created span
329+
# The parent is set at creation time. For current span info, parent_span_id is None.
330+
parent_span_id = None
331+
322332
# Check if span has is_pre_app_start attribute
323333
is_pre_app_start = False
324334
# Note: We can't easily read attributes from active span
@@ -327,6 +337,7 @@ def get_current_span_info() -> SpanInfo | None:
327337
return SpanInfo(
328338
trace_id=trace_id,
329339
span_id=span_id,
340+
parent_span_id=parent_span_id,
330341
span=active_span,
331342
context=otel_context.get_current(),
332343
is_pre_app_start=is_pre_app_start,

0 commit comments

Comments
 (0)