|
1 | 1 | import os |
| 2 | +import threading |
2 | 3 | import time |
3 | 4 | from typing import Any, cast |
4 | 5 | from unittest.mock import MagicMock, patch |
5 | 6 |
|
6 | 7 | import httpx |
7 | 8 | import pytest |
8 | 9 |
|
9 | | -from agents.tracing.processor_interface import TracingProcessor |
| 10 | +from agents.tracing import flush_traces, get_trace_provider |
| 11 | +from agents.tracing.processor_interface import TracingExporter, TracingProcessor |
10 | 12 | from agents.tracing.processors import BackendSpanExporter, BatchTraceProcessor |
| 13 | +from agents.tracing.provider import DefaultTraceProvider, TraceProvider |
11 | 14 | from agents.tracing.span_data import AgentSpanData |
12 | | -from agents.tracing.spans import SpanImpl |
13 | | -from agents.tracing.traces import TraceImpl |
| 15 | +from agents.tracing.spans import Span, SpanImpl |
| 16 | +from agents.tracing.traces import Trace, TraceImpl |
14 | 17 |
|
15 | 18 |
|
16 | 19 | def get_span(processor: TracingProcessor) -> SpanImpl[AgentSpanData]: |
@@ -123,6 +126,34 @@ def test_batch_trace_processor_force_flush(mocked_exporter): |
123 | 126 | processor.shutdown() |
124 | 127 |
|
125 | 128 |
|
| 129 | +def test_batch_trace_processor_force_flush_waits_for_in_flight_background_export(): |
| 130 | + export_started = threading.Event() |
| 131 | + export_continue = threading.Event() |
| 132 | + |
| 133 | + class BlockingExporter(TracingExporter): |
| 134 | + def export(self, items: list[Trace | Span[Any]]) -> None: |
| 135 | + export_started.set() |
| 136 | + assert export_continue.wait(timeout=2.0) |
| 137 | + |
| 138 | + processor = BatchTraceProcessor(exporter=BlockingExporter(), schedule_delay=0.01) |
| 139 | + processor.on_trace_start(get_trace(processor)) |
| 140 | + |
| 141 | + assert export_started.wait(timeout=2.0) |
| 142 | + |
| 143 | + flush_thread = threading.Thread(target=processor.force_flush) |
| 144 | + flush_thread.start() |
| 145 | + |
| 146 | + time.sleep(0.1) |
| 147 | + assert flush_thread.is_alive(), "force_flush() should wait for an in-flight export" |
| 148 | + |
| 149 | + export_continue.set() |
| 150 | + flush_thread.join(timeout=2.0) |
| 151 | + |
| 152 | + assert not flush_thread.is_alive() |
| 153 | + |
| 154 | + processor.shutdown() |
| 155 | + |
| 156 | + |
126 | 157 | def test_batch_trace_processor_shutdown_flushes(mocked_exporter): |
127 | 158 | processor = BatchTraceProcessor(exporter=mocked_exporter, schedule_delay=5.0) |
128 | 159 | processor.on_trace_start(get_trace(processor)) |
@@ -171,6 +202,100 @@ def test_batch_trace_processor_scheduled_export(mocked_exporter): |
171 | 202 | assert total_exported == 1, "Item should be exported after scheduled delay" |
172 | 203 |
|
173 | 204 |
|
| 205 | +def test_flush_traces_delegates_to_default_trace_provider(): |
| 206 | + provider = DefaultTraceProvider() |
| 207 | + mock_processor = MagicMock() |
| 208 | + provider.register_processor(mock_processor) |
| 209 | + |
| 210 | + with patch("agents.tracing.setup.GLOBAL_TRACE_PROVIDER", provider): |
| 211 | + flush_traces() |
| 212 | + |
| 213 | + mock_processor.force_flush.assert_called_once() |
| 214 | + |
| 215 | + |
| 216 | +def test_flush_traces_is_importable_from_top_level_agents_package(): |
| 217 | + from agents import flush_traces as top_level_flush_traces |
| 218 | + |
| 219 | + assert top_level_flush_traces is flush_traces |
| 220 | + |
| 221 | + |
| 222 | +def test_default_trace_provider_force_flush_respects_disabled_flag(): |
| 223 | + provider = DefaultTraceProvider() |
| 224 | + mock_processor = MagicMock() |
| 225 | + provider.register_processor(mock_processor) |
| 226 | + |
| 227 | + provider.set_disabled(True) |
| 228 | + provider.force_flush() |
| 229 | + |
| 230 | + mock_processor.force_flush.assert_not_called() |
| 231 | + |
| 232 | + |
| 233 | +def test_trace_provider_force_flush_and_shutdown_default_to_noops(): |
| 234 | + class MinimalProvider(TraceProvider): |
| 235 | + def register_processor(self, processor: TracingProcessor) -> None: |
| 236 | + pass |
| 237 | + |
| 238 | + def set_processors(self, processors: list[TracingProcessor]) -> None: |
| 239 | + pass |
| 240 | + |
| 241 | + def get_current_trace(self): |
| 242 | + return None |
| 243 | + |
| 244 | + def get_current_span(self): |
| 245 | + return None |
| 246 | + |
| 247 | + def set_disabled(self, disabled: bool) -> None: |
| 248 | + pass |
| 249 | + |
| 250 | + def time_iso(self) -> str: |
| 251 | + return "" |
| 252 | + |
| 253 | + def gen_trace_id(self) -> str: |
| 254 | + return "trace_123" |
| 255 | + |
| 256 | + def gen_span_id(self) -> str: |
| 257 | + return "span_123" |
| 258 | + |
| 259 | + def gen_group_id(self) -> str: |
| 260 | + return "group_123" |
| 261 | + |
| 262 | + def create_trace( |
| 263 | + self, |
| 264 | + name, |
| 265 | + trace_id=None, |
| 266 | + group_id=None, |
| 267 | + metadata=None, |
| 268 | + disabled=False, |
| 269 | + tracing=None, |
| 270 | + ): |
| 271 | + raise NotImplementedError |
| 272 | + |
| 273 | + def create_span(self, span_data, span_id=None, parent=None, disabled=False): |
| 274 | + raise NotImplementedError |
| 275 | + |
| 276 | + provider = MinimalProvider() |
| 277 | + provider.force_flush() |
| 278 | + provider.shutdown() |
| 279 | + |
| 280 | + |
| 281 | +def test_get_trace_provider_force_flush_flushes_default_processor(mocked_exporter): |
| 282 | + provider = DefaultTraceProvider() |
| 283 | + processor = BatchTraceProcessor(exporter=mocked_exporter, schedule_delay=60.0) |
| 284 | + provider.register_processor(processor) |
| 285 | + |
| 286 | + with patch("agents.tracing.setup.GLOBAL_TRACE_PROVIDER", provider): |
| 287 | + processor.on_trace_start(get_trace(processor)) |
| 288 | + processor.on_span_end(get_span(processor)) |
| 289 | + |
| 290 | + get_trace_provider().force_flush() |
| 291 | + |
| 292 | + total_exported = sum( |
| 293 | + len(call_args[0][0]) for call_args in mocked_exporter.export.call_args_list |
| 294 | + ) |
| 295 | + assert total_exported == 2 |
| 296 | + processor.shutdown() |
| 297 | + |
| 298 | + |
174 | 299 | @pytest.fixture |
175 | 300 | def patched_time_sleep(): |
176 | 301 | """ |
|
0 commit comments