From 10205938ea031496f4d0e2e904fce66f79427c6f Mon Sep 17 00:00:00 2001 From: cijothomas Date: Wed, 29 Jul 2026 08:18:52 -0700 Subject: [PATCH 1/8] Count processor.processed at exporter-submit time, not export completion --- .../sdk/_logs/_internal/export/__init__.py | 8 +-- .../sdk/_shared_internal/__init__.py | 28 ++++----- .../_shared_internal/_processor_metrics.py | 15 ++--- .../sdk/trace/export/__init__.py | 10 ++-- opentelemetry-sdk/tests/logs/test_export.py | 59 ++++++------------- .../tests/trace/export/test_export.py | 58 ++++++------------ 6 files changed, 62 insertions(+), 116 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py index b9763d92be9..589b7128ff1 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -230,7 +230,6 @@ def on_emit(self, log_record: ReadWriteLogRecord): set_value(_ON_EMIT_RECURSION_COUNT_KEY, cnt + 1), # pyright: ignore[reportOperatorIssue] ) ) - error: Exception | None = None try: if self._shutdown: _logger.warning("Processor is already shutdown, ignoring call") @@ -248,12 +247,13 @@ def on_emit(self, log_record: ReadWriteLogRecord): instrumentation_scope=log_record.instrumentation_scope, limits=log_record.limits, ) + # Count as processed when submitting to the exporter, independent + # of the export outcome. + self._metrics.finish_items(1) self._exporter.export((readable_log_record,)) - except Exception as err: # pylint: disable=broad-exception-caught - error = err + except Exception: # pylint: disable=broad-exception-caught _logger.exception("Exception while exporting logs.") finally: - self._metrics.finish_items(1, error) detach(token) def shutdown(self): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py index 3e2b8a263a4..9a36ddd6290 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py @@ -172,27 +172,21 @@ def _export(self, batch_strategy: BatchExportStrategy) -> None: while self._should_export_batch(batch_strategy, iteration): iteration += 1 token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) - error: Exception | None = None - count = 0 + count = min( + self._max_export_batch_size, + len(self._queue), + ) + # Oldest records are at the back, so pop from there. + batch = [self._queue.pop() for _ in range(count)] + # Count records as processed when the batch is submitted to the + # exporter, independent of the export outcome. + self._metrics.finish_items(count) try: - count = min( - self._max_export_batch_size, - len(self._queue), - ) - self._exporter.export( - [ - # Oldest records are at the back, so pop from there. - self._queue.pop() - for _ in range(count) - ] - ) - except Exception as err: # pylint: disable=broad-exception-caught - error = err + self._exporter.export(batch) + except Exception: # pylint: disable=broad-exception-caught _logger.exception( "Exception while exporting %s.", self._exporting ) - finally: - self._metrics.finish_items(count, error) detach(token) def emit(self, data: Telemetry) -> None: diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/_processor_metrics.py b/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/_processor_metrics.py index 9f7e7d7c068..59dfc2f65bd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/_processor_metrics.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/_processor_metrics.py @@ -33,7 +33,7 @@ def register_queue_size( def drop_items(self, count: int) -> None: ... - def finish_items(self, count: int, error: Exception | None) -> None: ... + def finish_items(self, count: int) -> None: ... class NoOpProcessorMetrics: @@ -43,7 +43,7 @@ def register_queue_size(self, get_queue_size: Callable[[], int]) -> None: def drop_items(self, count: int) -> None: pass - def finish_items(self, count: int, error: Exception | None) -> None: + def finish_items(self, count: int) -> None: pass @@ -115,15 +115,8 @@ def record_queue_size( def drop_items(self, count: int) -> None: self._processed.add(count, self._dropped_attrs) - def finish_items(self, count: int, error: Exception | None) -> None: - if not error: - self._processed.add(count, self._standard_attrs) - return - attrs = { - **self._standard_attrs, - ERROR_TYPE: type(error).__name__, - } - self._processed.add(count, attrs) + def finish_items(self, count: int) -> None: + self._processed.add(count, self._standard_attrs) def create_processor_metrics( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index ad8f57840a0..77daa49313a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -124,16 +124,16 @@ def on_end(self, span: ReadableSpan) -> None: if not (span.context and span.context.trace_flags.sampled): return token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) - error: Exception | None = None try: + # Count as processed when submitting to the exporter, independent + # of the export outcome. + self._metrics.finish_items(1) self.span_exporter.export((span,)) # pylint: disable=broad-exception-caught - except Exception as err: - error = err + except Exception: logger.exception("Exception while exporting Span.") finally: - self._metrics.finish_items(1, error) - detach(token) + detach(token) def shutdown(self) -> None: self.span_exporter.shutdown() diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index 5d8b4328cea..d5f7bfea711 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -433,13 +433,12 @@ def export_logs(_logs): metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 1) self.assertEqual(metrics[0].name, "otel.sdk.processor.log.processed") - processed_data_points = sorted( - metrics[0].data.data_points, - key=lambda dp: dp.attributes.get("error.type", ""), - ) - self.assertEqual(len(processed_data_points), 2) + processed_data_points = metrics[0].data.data_points + self.assertEqual(len(processed_data_points), 1) processed_data_point0 = processed_data_points[0] - self.assertEqual(processed_data_point0.value, 2) + # All 3 logs are counted as processed when submitted to the exporter, + # independent of the export outcome (the 3rd export fails). + self.assertEqual(processed_data_point0.value, 3) self.assertEqual( processed_data_point0.attributes["otel.component.type"], "simple_log_processor", @@ -450,20 +449,6 @@ def export_logs(_logs): ) ) self.assertIsNone(processed_data_point0.attributes.get("error.type")) - processed_data_point1 = processed_data_points[1] - self.assertEqual(processed_data_point1.value, 1) - self.assertEqual( - processed_data_point1.attributes["otel.component.type"], - "simple_log_processor", - ) - self.assertTrue( - processed_data_point1.attributes["otel.component.name"].startswith( - "simple_log_processor/" - ) - ) - self.assertEqual( - processed_data_point1.attributes["error.type"], "RuntimeError" - ) # Many more test cases for the BatchLogRecordProcessor exist under @@ -746,7 +731,9 @@ def export_logs(_logs): metrics[0].data.data_points, key=lambda dp: dp.attributes.get("error.type", ""), ) - self.assertEqual(len(processed_data_points), 1) + # "foo" is counted as processed when submitted to the exporter (before + # its export call blocks); "baz" is dropped due to a full queue. + self.assertEqual(len(processed_data_points), 2) processed_data_point0 = processed_data_points[0] self.assertEqual(processed_data_point0.value, 1) self.assertEqual( @@ -758,8 +745,12 @@ def export_logs(_logs): "batching_log_processor/" ) ) + self.assertIsNone(processed_data_point0.attributes.get("error.type")) + processed_data_point_queue_full = processed_data_points[1] + self.assertEqual(processed_data_point_queue_full.value, 1) self.assertEqual( - processed_data_point0.attributes.get("error.type"), "queue_full" + processed_data_point_queue_full.attributes.get("error.type"), + "queue_full", ) self.assertEqual( metrics[1].name, "otel.sdk.processor.log.queue.capacity" @@ -806,9 +797,12 @@ def export_logs(_logs): metrics[0].data.data_points, key=lambda dp: dp.attributes.get("error.type", ""), ) - self.assertEqual(len(processed_data_points), 3) + # "foo", "bar" and "failed" are all counted as processed when submitted + # to the exporter, independent of the export outcome ("failed" raises). + # "baz" remains a queue_full drop. + self.assertEqual(len(processed_data_points), 2) processed_data_point0 = processed_data_points[0] - self.assertEqual(processed_data_point0.value, 2) + self.assertEqual(processed_data_point0.value, 3) self.assertEqual( processed_data_point0.attributes["otel.component.type"], "batching_log_processor", @@ -831,22 +825,7 @@ def export_logs(_logs): ) ) self.assertEqual( - processed_data_point1.attributes.get("error.type"), - "BrokenPipeError", - ) - processed_data_point2 = processed_data_points[2] - self.assertEqual(processed_data_point2.value, 1) - self.assertEqual( - processed_data_point2.attributes["otel.component.type"], - "batching_log_processor", - ) - self.assertTrue( - processed_data_point2.attributes["otel.component.name"].startswith( - "batching_log_processor/" - ) - ) - self.assertEqual( - processed_data_point2.attributes.get("error.type"), "queue_full" + processed_data_point1.attributes.get("error.type"), "queue_full" ) self.assertEqual( metrics[1].name, "otel.sdk.processor.log.queue.capacity" diff --git a/opentelemetry-sdk/tests/trace/export/test_export.py b/opentelemetry-sdk/tests/trace/export/test_export.py index d67aceb19f8..d08bdbadcdb 100644 --- a/opentelemetry-sdk/tests/trace/export/test_export.py +++ b/opentelemetry-sdk/tests/trace/export/test_export.py @@ -172,13 +172,12 @@ def export_spans(_spans): metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) self.assertEqual(len(metrics), 1) self.assertEqual(metrics[0].name, "otel.sdk.processor.span.processed") - processed_data_points = sorted( - metrics[0].data.data_points, - key=lambda dp: dp.attributes.get("error.type", ""), - ) - self.assertEqual(len(processed_data_points), 2) + processed_data_points = metrics[0].data.data_points + self.assertEqual(len(processed_data_points), 1) processed_data_point0 = processed_data_points[0] - self.assertEqual(processed_data_point0.value, 2) + # All 3 spans are counted as processed when submitted to the exporter, + # independent of the export outcome (the 3rd export fails). + self.assertEqual(processed_data_point0.value, 3) self.assertEqual( processed_data_point0.attributes["otel.component.type"], "simple_span_processor", @@ -189,20 +188,6 @@ def export_spans(_spans): ) ) self.assertIsNone(processed_data_point0.attributes.get("error.type")) - processed_data_point1 = processed_data_points[1] - self.assertEqual(processed_data_point1.value, 1) - self.assertEqual( - processed_data_point1.attributes["otel.component.type"], - "simple_span_processor", - ) - self.assertTrue( - processed_data_point1.attributes["otel.component.name"].startswith( - "simple_span_processor/" - ) - ) - self.assertEqual( - processed_data_point1.attributes["error.type"], "RuntimeError" - ) # Many more test cases for the BatchSpanProcessor exist under @@ -447,7 +432,9 @@ def export_spans(_spans): metrics[0].data.data_points, key=lambda dp: dp.attributes.get("error.type", ""), ) - self.assertEqual(len(processed_data_points), 1) + # "foo" is counted as processed when submitted to the exporter (before + # its export call blocks); "baz" is dropped due to a full queue. + self.assertEqual(len(processed_data_points), 2) processed_data_point0 = processed_data_points[0] self.assertEqual(processed_data_point0.value, 1) self.assertEqual( @@ -459,8 +446,12 @@ def export_spans(_spans): "batching_span_processor/" ) ) + self.assertIsNone(processed_data_point0.attributes.get("error.type")) + processed_data_point_queue_full = processed_data_points[1] + self.assertEqual(processed_data_point_queue_full.value, 1) self.assertEqual( - processed_data_point0.attributes.get("error.type"), "queue_full" + processed_data_point_queue_full.attributes.get("error.type"), + "queue_full", ) self.assertEqual( metrics[1].name, "otel.sdk.processor.span.queue.capacity" @@ -508,9 +499,12 @@ def export_spans(_spans): metrics[0].data.data_points, key=lambda dp: dp.attributes.get("error.type", ""), ) - self.assertEqual(len(processed_data_points), 3) + # "foo", "bar" and "failed" are all counted as processed when submitted + # to the exporter, independent of the export outcome ("failed" raises). + # "baz" remains a queue_full drop. + self.assertEqual(len(processed_data_points), 2) processed_data_point0 = processed_data_points[0] - self.assertEqual(processed_data_point0.value, 2) + self.assertEqual(processed_data_point0.value, 3) self.assertEqual( processed_data_point0.attributes["otel.component.type"], "batching_span_processor", @@ -533,21 +527,7 @@ def export_spans(_spans): ) ) self.assertEqual( - processed_data_point1.attributes.get("error.type"), "ValueError" - ) - processed_data_point2 = processed_data_points[2] - self.assertEqual(processed_data_point2.value, 1) - self.assertEqual( - processed_data_point2.attributes["otel.component.type"], - "batching_span_processor", - ) - self.assertTrue( - processed_data_point2.attributes["otel.component.name"].startswith( - "batching_span_processor/" - ) - ) - self.assertEqual( - processed_data_point2.attributes.get("error.type"), "queue_full" + processed_data_point1.attributes.get("error.type"), "queue_full" ) self.assertEqual( metrics[1].name, "otel.sdk.processor.span.queue.capacity" From b81750c7faa3382af7f988d77e7ec525e675627e Mon Sep 17 00:00:00 2001 From: cijothomas Date: Wed, 29 Jul 2026 08:22:00 -0700 Subject: [PATCH 2/8] Add changelog fragment --- .changelog/5472.fixed | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .changelog/5472.fixed diff --git a/.changelog/5472.fixed b/.changelog/5472.fixed new file mode 100644 index 00000000000..a3d55a1518b --- /dev/null +++ b/.changelog/5472.fixed @@ -0,0 +1,4 @@ +`opentelemetry-sdk`: count `otel.sdk.processor.{span,log}.processed` when the +processor submits a batch to the exporter instead of after export completes, +and stop stamping exporter failures onto this metric as `error.type` +([#5472](https://github.com/open-telemetry/opentelemetry-python/pull/5472)) From 5d5cd36440f33564562c3c11dae67004cfec86c9 Mon Sep 17 00:00:00 2001 From: cijothomas Date: Wed, 29 Jul 2026 08:24:54 -0700 Subject: [PATCH 3/8] Trim comments and fix changelog wording --- .changelog/5472.fixed | 2 +- .../src/opentelemetry/sdk/_logs/_internal/export/__init__.py | 3 +-- .../src/opentelemetry/sdk/_shared_internal/__init__.py | 3 +-- .../src/opentelemetry/sdk/trace/export/__init__.py | 3 +-- 4 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.changelog/5472.fixed b/.changelog/5472.fixed index a3d55a1518b..8803e9d9afe 100644 --- a/.changelog/5472.fixed +++ b/.changelog/5472.fixed @@ -1,4 +1,4 @@ `opentelemetry-sdk`: count `otel.sdk.processor.{span,log}.processed` when the -processor submits a batch to the exporter instead of after export completes, +processor submits records to the exporter instead of after export completes, and stop stamping exporter failures onto this metric as `error.type` ([#5472](https://github.com/open-telemetry/opentelemetry-python/pull/5472)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py index 589b7128ff1..8759f2dfae9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -247,8 +247,7 @@ def on_emit(self, log_record: ReadWriteLogRecord): instrumentation_scope=log_record.instrumentation_scope, limits=log_record.limits, ) - # Count as processed when submitting to the exporter, independent - # of the export outcome. + # Record on submission to the exporter. self._metrics.finish_items(1) self._exporter.export((readable_log_record,)) except Exception: # pylint: disable=broad-exception-caught diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py index 9a36ddd6290..77c6027f517 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_shared_internal/__init__.py @@ -178,8 +178,7 @@ def _export(self, batch_strategy: BatchExportStrategy) -> None: ) # Oldest records are at the back, so pop from there. batch = [self._queue.pop() for _ in range(count)] - # Count records as processed when the batch is submitted to the - # exporter, independent of the export outcome. + # Record on submission to the exporter. self._metrics.finish_items(count) try: self._exporter.export(batch) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index 77daa49313a..c40e8326b2a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -125,8 +125,7 @@ def on_end(self, span: ReadableSpan) -> None: return token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) try: - # Count as processed when submitting to the exporter, independent - # of the export outcome. + # Record on submission to the exporter. self._metrics.finish_items(1) self.span_exporter.export((span,)) # pylint: disable=broad-exception-caught From 36d277aa649af27eaf7c729226cd990a9799af3a Mon Sep 17 00:00:00 2001 From: cijothomas Date: Wed, 29 Jul 2026 08:25:37 -0700 Subject: [PATCH 4/8] Clarify changelog covers simple and batch processors --- .changelog/5472.fixed | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.changelog/5472.fixed b/.changelog/5472.fixed index 8803e9d9afe..078c65efee2 100644 --- a/.changelog/5472.fixed +++ b/.changelog/5472.fixed @@ -1,4 +1,5 @@ -`opentelemetry-sdk`: count `otel.sdk.processor.{span,log}.processed` when the -processor submits records to the exporter instead of after export completes, -and stop stamping exporter failures onto this metric as `error.type` +`opentelemetry-sdk`: for both the simple and batch span/log processors, count +`otel.sdk.processor.{span,log}.processed` when the processor submits records to +the exporter instead of after export completes, and stop stamping exporter +failures onto this metric as `error.type` ([#5472](https://github.com/open-telemetry/opentelemetry-python/pull/5472)) From 05b0a3128d2f54afecf5546ee98bba8785ac57d7 Mon Sep 17 00:00:00 2001 From: cijothomas Date: Fri, 31 Jul 2026 14:18:17 -0700 Subject: [PATCH 5/8] Use single-line changelog fragment without redundant PR link Assisted-by: Claude Opus 4.8 --- .changelog/5472.fixed | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.changelog/5472.fixed b/.changelog/5472.fixed index 078c65efee2..915535e82d1 100644 --- a/.changelog/5472.fixed +++ b/.changelog/5472.fixed @@ -1,5 +1 @@ -`opentelemetry-sdk`: for both the simple and batch span/log processors, count -`otel.sdk.processor.{span,log}.processed` when the processor submits records to -the exporter instead of after export completes, and stop stamping exporter -failures onto this metric as `error.type` -([#5472](https://github.com/open-telemetry/opentelemetry-python/pull/5472)) +`opentelemetry-sdk`: for both the simple and batch span/log processors, count `otel.sdk.processor.{span,log}.processed` when the processor submits records to the exporter instead of after export completes, and stop stamping exporter failures onto this metric as `error.type` From 7dadb2f96bd64ecd31557c2485f1fe6410261c01 Mon Sep 17 00:00:00 2001 From: cijothomas Date: Fri, 31 Jul 2026 14:26:30 -0700 Subject: [PATCH 6/8] Add test that shutdown-dropped records are not counted as processed Assisted-by: Claude Opus 4.8 --- opentelemetry-sdk/tests/logs/test_export.py | 36 +++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index d5f7bfea711..dc23477b8b2 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -450,6 +450,42 @@ def export_logs(_logs): ) self.assertIsNone(processed_data_point0.attributes.get("error.type")) + @patch.dict( + "os.environ", {OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED: "true"} + ) + def test_metrics_not_counted_after_shutdown(self): + metric_reader = InMemoryMetricReader() + meter_provider = MeterProvider(metric_readers=[metric_reader]) + + exporter = mock.MagicMock() + exporter.export.return_value = LogRecordExportResult.SUCCESS + processor = SimpleLogRecordProcessor( + exporter, meter_provider=meter_provider + ) + provider = LoggerProvider() + provider.add_log_record_processor(processor) + logger = provider.get_logger("test_shutdown_metrics") + + logger.emit(LogRecord(body="foo", severity_number=SeverityNumber.WARN)) + + # Shut only the processor down; the record emitted afterwards hits the + # already-shutdown early return and must not be counted as processed. + processor.shutdown() + logger.emit(LogRecord(body="bar", severity_number=SeverityNumber.WARN)) + + metrics_data = metric_reader.get_metrics_data() + scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] + metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) + self.assertEqual(len(metrics), 1) + self.assertEqual(metrics[0].name, "otel.sdk.processor.log.processed") + processed_data_points = metrics[0].data.data_points + self.assertEqual(len(processed_data_points), 1) + self.assertEqual(processed_data_points[0].value, 1) + self.assertIsNone( + processed_data_points[0].attributes.get("error.type") + ) + self.assertEqual(exporter.export.call_count, 1) + # Many more test cases for the BatchLogRecordProcessor exist under # opentelemetry-sdk/tests/shared_internal/test_batch_processor.py. From c14475ac0c8147992a6e911f7edd2f257ccca8ea Mon Sep 17 00:00:00 2001 From: cijothomas Date: Tue, 4 Aug 2026 10:05:23 -0700 Subject: [PATCH 7/8] Move SimpleSpanProcessor finish_items outside the export try/except Assisted-by: Claude Opus 4.8 --- .../src/opentelemetry/sdk/trace/export/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index c40e8326b2a..7daaa638d78 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -124,9 +124,9 @@ def on_end(self, span: ReadableSpan) -> None: if not (span.context and span.context.trace_flags.sampled): return token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True)) + # Record on submission to the exporter. + self._metrics.finish_items(1) try: - # Record on submission to the exporter. - self._metrics.finish_items(1) self.span_exporter.export((span,)) # pylint: disable=broad-exception-caught except Exception: From 019d26276ef98b31de6dfad3b12e89d5456e3d0c Mon Sep 17 00:00:00 2001 From: cijothomas Date: Tue, 4 Aug 2026 11:07:40 -0700 Subject: [PATCH 8/8] Simplify shutdown metrics test to call on_emit directly Assisted-by: Claude Opus 4.8 --- opentelemetry-sdk/tests/logs/test_export.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/opentelemetry-sdk/tests/logs/test_export.py b/opentelemetry-sdk/tests/logs/test_export.py index dc23477b8b2..7fd99b228da 100644 --- a/opentelemetry-sdk/tests/logs/test_export.py +++ b/opentelemetry-sdk/tests/logs/test_export.py @@ -462,20 +462,17 @@ def test_metrics_not_counted_after_shutdown(self): processor = SimpleLogRecordProcessor( exporter, meter_provider=meter_provider ) - provider = LoggerProvider() - provider.add_log_record_processor(processor) - logger = provider.get_logger("test_shutdown_metrics") - logger.emit(LogRecord(body="foo", severity_number=SeverityNumber.WARN)) + processor.on_emit(EMPTY_LOG) # Shut only the processor down; the record emitted afterwards hits the # already-shutdown early return and must not be counted as processed. processor.shutdown() - logger.emit(LogRecord(body="bar", severity_number=SeverityNumber.WARN)) + processor.on_emit(EMPTY_LOG) metrics_data = metric_reader.get_metrics_data() scope_metrics = metrics_data.resource_metrics[0].scope_metrics[0] - metrics = sorted(scope_metrics.metrics, key=lambda m: m.name) + metrics = scope_metrics.metrics self.assertEqual(len(metrics), 1) self.assertEqual(metrics[0].name, "otel.sdk.processor.log.processed") processed_data_points = metrics[0].data.data_points