| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | import asyncio |
| 4 | |
| 5 | import pytest |
| 6 | from opentelemetry.sdk.environment_variables import OTEL_EXPORTER_OTLP_TRACES_INSECURE |
| 7 | |
| 8 | from tests.tracing.conftest import FAKE_TRACE_SERVER_ADDRESS, FakeTraceService |
| 9 | from vllm.tracing import init_tracer, instrument, is_otel_available |
| 10 | |
| 11 | # Skip everything if OTel is missing |
| 12 | pytestmark = pytest.mark.skipif(not is_otel_available(), reason="OTel required") |
| 13 | |
| 14 | |
| 15 | class TestCoreInstrumentation: |
| 16 | """Focuses on the @instrument decorator's ability to capture execution data.""" |
| 17 | |
| 18 | @pytest.fixture(autouse=True) |
| 19 | def setup_tracing(self, monkeypatch): |
| 20 | monkeypatch.setenv(OTEL_EXPORTER_OTLP_TRACES_INSECURE, "true") |
| 21 | init_tracer("test.core", FAKE_TRACE_SERVER_ADDRESS) |
| 22 | |
| 23 | def test_decorator_captures_sync_and_async(self, trace_service: FakeTraceService): |
| 24 | """Verify basic span creation for both sync and async functions.""" |
| 25 | |
| 26 | @instrument(span_name="sync_task") |
| 27 | def sync_task(): |
| 28 | return True |
| 29 | |
| 30 | @instrument(span_name="async_task") |
| 31 | async def async_task(): |
| 32 | return True |
| 33 | |
| 34 | sync_task() |
| 35 | asyncio.run(async_task()) |
| 36 | |
| 37 | assert trace_service.wait_for_spans(count=2) |
| 38 | span_names = [s["name"] for s in trace_service.get_all_spans()] |
| 39 | assert "sync_task" in span_names |
| 40 | assert "async_task" in span_names |
| 41 | |
| 42 | def test_nested_spans_hierarchy(self, trace_service: FakeTraceService): |
| 43 | """Verify that nested calls create a parent-child relationship.""" |
| 44 | |
| 45 | @instrument(span_name="child") |
| 46 | def child(): |
| 47 | pass |
| 48 | |
| 49 | @instrument(span_name="parent") |
| 50 | def parent(): |
| 51 | child() |
| 52 | |
| 53 | parent() |
| 54 | |
| 55 | assert trace_service.wait_for_spans(count=2) |
| 56 | spans = trace_service.get_all_spans() |
| 57 | parent_span = next(s for s in spans if s["name"] == "parent") |
| 58 | child_span = next(s for s in spans if s["name"] == "child") |
| 59 | |
| 60 | assert child_span["parent_span_id"] == parent_span["span_id"] |
| 61 | |
| 62 | |
| 63 | class TestInterProcessPropagation: |
| 64 | """Test the propagation of trace context between processes.""" |
| 65 | |
| 66 | def test_pickup_external_context(self, monkeypatch, trace_service): |
| 67 | """Test that vLLM attaches to an existing trace ID if in environment.""" |
| 68 | monkeypatch.setenv(OTEL_EXPORTER_OTLP_TRACES_INSECURE, "true") |
| 69 | |
| 70 | # Manually simulate an external parent trace ID |
| 71 | fake_trace_id = "4bf92f3577b34da6a3ce929d0e0e4736" |
| 72 | fake_parent_id = "00f067aa0ba902b7" |
| 73 | monkeypatch.setenv("traceparent", f"00-{fake_trace_id}-{fake_parent_id}-01") |
| 74 | |
| 75 | init_tracer("test.external", FAKE_TRACE_SERVER_ADDRESS) |
| 76 | |
| 77 | @instrument(span_name="follower") |
| 78 | def follower_func(): |
| 79 | pass |
| 80 | |
| 81 | follower_func() |
| 82 | |
| 83 | assert trace_service.wait_for_spans(count=1) |
| 84 | span = trace_service.get_all_spans()[0] |
| 85 | |
| 86 | assert span["trace_id"] == fake_trace_id |
| 87 | assert span["parent_span_id"] == fake_parent_id |