Skip to content

Commit 9a275be

Browse files
feat: refactor instrumentations to use span/mode utils + add sampling (#18)
1 parent 00dcd88 commit 9a275be

10 files changed

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

drift/core/tracing/td_span_processor.py

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from opentelemetry.sdk.trace import ReadableSpan, SpanProcessor
1414
from opentelemetry.trace import Span
1515

16-
from ..sampling import should_sample
1716
from ..trace_blocking_manager import TraceBlockingManager, should_block_span
1817
from ..types import TD_INSTRUMENTATION_LIBRARY_NAME, TuskDriftMode, replay_trace_id_context
1918
from ..types import SpanKind as TdSpanKind
@@ -141,10 +140,6 @@ def on_end(self, span: ReadableSpan) -> None:
141140
if should_block_span(clean_span):
142141
return
143142

144-
# Apply sampling logic
145-
if not should_sample(self._sampling_rate, self._app_ready):
146-
return
147-
148143
# Validate protobuf serialization
149144
try:
150145
clean_span.to_proto()

0 commit comments

Comments
 (0)