Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .changelog/5512.changed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
`opentelemetry-sdk`: `SimpleSpanProcessor` now drops spans ended after `shutdown()` instead of passing them to the exporter, and counts them on `otel.sdk.processor.span.processed` with `error.type=already_shutdown`.
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ def __init__(
meter_provider: MeterProvider | None = None,
):
self.span_exporter = span_exporter
self._shutdown = False
self._metrics = create_processor_metrics(
"traces",
OtelComponentTypeValues.SIMPLE_SPAN_PROCESSOR,
Expand All @@ -115,6 +116,10 @@ def _on_ending(self, span: Span) -> None:
def on_end(self, span: ReadableSpan) -> None:
if not (span.context and span.context.trace_flags.sampled):
return
if self._shutdown:
logger.warning("Processor is already shutdown, ignoring call")
self._metrics.drop_items(1, "already_shutdown")
return
token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True))
# Record on submission to the exporter.
self._metrics.finish_items(1)
Expand All @@ -127,6 +132,7 @@ def on_end(self, span: ReadableSpan) -> None:
detach(token)

def shutdown(self) -> None:
self._shutdown = True
self.span_exporter.shutdown()

def force_flush(self, timeout_millis: int = 30000) -> bool:
Expand Down
38 changes: 38 additions & 0 deletions opentelemetry-sdk/tests/trace/export/test_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,44 @@ def export_spans(_spans):
self.assertTrue(processed_data_point0.attributes["otel.component.name"].startswith("simple_span_processor/"))
self.assertIsNone(processed_data_point0.attributes.get("error.type"))

@mock.patch.dict("os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"})
def test_metrics_already_shutdown(self):
metric_reader = InMemoryMetricReader()
meter_provider = MeterProvider(metric_readers=[metric_reader])

exporter = mock.MagicMock()
exporter.export.return_value = export.SpanExportResult.SUCCESS
span_processor = export.SimpleSpanProcessor(exporter, meter_provider=meter_provider)
provider = trace.TracerProvider()
tracer = provider.get_tracer(__name__)
provider.add_span_processor(span_processor)

# Ended before shutdown: submitted to the exporter and counted as
# successfully processed.
with tracer.start_as_current_span("foo"):
pass
span_processor.shutdown()

# Ended after shutdown: dropped and counted as already_shutdown.
with tracer.start_as_current_span("bar"):
pass

metrics_data = metric_reader.get_metrics_data()
scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0]
processed = next(m for m in scope_metrics.metrics if m.name == "otel.sdk.processor.span.processed")
data_points = sorted(
processed.data.data_points,
key=lambda dp: dp.attributes.get("error.type", ""),
)
self.assertEqual(len(data_points), 2)
self.assertEqual(data_points[0].value, 1)
self.assertIsNone(data_points[0].attributes.get("error.type"))
self.assertEqual(data_points[1].value, 1)
self.assertEqual(data_points[1].attributes.get("error.type"), "already_shutdown")
# Only the pre-shutdown span is submitted to the exporter; the
# post-shutdown span is dropped, not exported.
self.assertEqual(exporter.export.call_count, 1)


# Many more test cases for the BatchSpanProcessor exist under
# opentelemetry-sdk/tests/shared_internal/test_batch_processor.py.
Expand Down
Loading