|
1 | 1 | import asyncio |
| 2 | +import ctypes |
2 | 3 | import sys |
3 | 4 | import threading |
4 | 5 | import time |
@@ -182,32 +183,55 @@ async def coro(i): |
182 | 183 | reason="requires the CPython 3.14 context watcher on Linux", |
183 | 184 | ) |
184 | 185 | @pytest.mark.asyncio |
185 | | -async def test_otel_thread_context_follows_async_context_switches(oteltracer, monkeypatch): |
186 | | - from ddtrace.internal.opentelemetry import thread_context |
187 | | - |
188 | | - published_spans = [] |
189 | | - monkeypatch.setattr( |
190 | | - thread_context, |
191 | | - "update_otel_thread_context", |
192 | | - lambda span, local_root, trace_flags: published_spans.append(span), |
193 | | - ) |
| 186 | +async def test_otel_thread_context_follows_async_context_switches(oteltracer): |
| 187 | + """The published thread context follows each OTel span as asyncio switches tasks.""" |
| 188 | + from ddtrace.internal.native import _native |
| 189 | + |
| 190 | + class _ThreadContextRecord(ctypes.Structure): |
| 191 | + _fields_ = [ |
| 192 | + ("trace_id", ctypes.c_ubyte * 16), |
| 193 | + ("span_id", ctypes.c_ubyte * 8), |
| 194 | + ("valid", ctypes.c_ubyte), |
| 195 | + ] |
| 196 | + |
| 197 | + native_library = ctypes.CDLL(_native.__file__) |
| 198 | + |
| 199 | + def published_context(): |
| 200 | + slot = ctypes.c_void_p.in_dll(native_library, "otel_thread_ctx_v1") |
| 201 | + if slot.value is None: |
| 202 | + return None |
| 203 | + |
| 204 | + record = _ThreadContextRecord.from_address(slot.value) |
| 205 | + if not record.valid: |
| 206 | + return None |
| 207 | + return int.from_bytes(record.trace_id, byteorder="big"), int.from_bytes(record.span_id, byteorder="big") |
| 208 | + |
| 209 | + def span_context(span): |
| 210 | + context = span.get_span_context() |
| 211 | + return context.trace_id, context.span_id |
| 212 | + |
194 | 213 | first_started = asyncio.Event() |
195 | 214 | second_started = asyncio.Event() |
196 | 215 |
|
197 | 216 | async def first(): |
198 | 217 | with oteltracer.start_as_current_span("first") as first_span: |
| 218 | + expected_context = span_context(first_span) |
| 219 | + assert published_context() == expected_context |
199 | 220 | first_started.set() |
200 | 221 | await second_started.wait() |
201 | | - assert published_spans and published_spans[-1] is first_span._ddspan |
| 222 | + assert published_context() == expected_context |
202 | 223 |
|
203 | 224 | async def second(): |
204 | 225 | await first_started.wait() |
205 | | - with oteltracer.start_as_current_span("second"): |
206 | | - published_spans.clear() |
| 226 | + with oteltracer.start_as_current_span("second") as second_span: |
| 227 | + expected_context = span_context(second_span) |
| 228 | + assert published_context() == expected_context |
207 | 229 | second_started.set() |
208 | 230 | await asyncio.sleep(0) |
| 231 | + assert published_context() == expected_context |
209 | 232 |
|
210 | 233 | await asyncio.gather(first(), second()) |
| 234 | + assert published_context() is None |
211 | 235 |
|
212 | 236 |
|
213 | 237 | def test_otel_get_current_span(oteltracer): |
|
0 commit comments