diff --git a/.changelog/5784.added b/.changelog/5784.added new file mode 100644 index 00000000000..e6f98e80aaa --- /dev/null +++ b/.changelog/5784.added @@ -0,0 +1 @@ +`opentelemetry-sdk`: add additional logs benchmarks and improve performance for disabled loggers diff --git a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py index 4c049f92e5e..203d0a9314b 100644 --- a/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py +++ b/opentelemetry-api/src/opentelemetry/_logs/_internal/__init__.py @@ -53,6 +53,21 @@ class LogRecord(ABC): pertinent to the event being logged. """ + __slots__ = ( + "timestamp", + "observed_timestamp", + "context", + "trace_id", + "span_id", + "trace_flags", + "severity_text", + "severity_number", + "body", + "attributes", + "event_name", + "exception", + ) + @overload def __init__( self, diff --git a/opentelemetry-api/tests/logs/test_log_record.py b/opentelemetry-api/tests/logs/test_log_record.py index 2b6f893374c..9fa65140f48 100644 --- a/opentelemetry-api/tests/logs/test_log_record.py +++ b/opentelemetry-api/tests/logs/test_log_record.py @@ -5,8 +5,21 @@ from unittest.mock import patch from opentelemetry._logs import LogRecord +from opentelemetry.trace import ( + NonRecordingSpan, + SpanContext, + TraceFlags, + set_span_in_context, + use_span, +) OBSERVED_TIMESTAMP = "OBSERVED_TIMESTAMP" +SPAN_CONTEXT = SpanContext( + trace_id=0x000000000000000000000000DEADBEEF, + span_id=0x00000000DEADBEF0, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.SAMPLED), +) class TestLogRecord(unittest.TestCase): @@ -19,3 +32,26 @@ def test_log_record_exception(self): exc = ValueError("boom") log_record = LogRecord(exception=exc) self.assertIs(log_record.exception, exc) + + def test_log_record_span_context_fields_from_context(self): + log_record = LogRecord(context=set_span_in_context(NonRecordingSpan(SPAN_CONTEXT))) + self.assertEqual(log_record.trace_id, SPAN_CONTEXT.trace_id) + self.assertEqual(log_record.span_id, SPAN_CONTEXT.span_id) + self.assertEqual(log_record.trace_flags, SPAN_CONTEXT.trace_flags) + + def test_log_record_explicit_span_context_fields(self): + span_context = SpanContext( + trace_id=SPAN_CONTEXT.trace_id, + span_id=SPAN_CONTEXT.span_id, + is_remote=False, + trace_flags=TraceFlags(TraceFlags.DEFAULT), + ) + with use_span(NonRecordingSpan(span_context)): + log_record = LogRecord( + trace_id=1, + span_id=2, + trace_flags=TraceFlags(TraceFlags.SAMPLED), + ) + self.assertEqual(log_record.trace_id, 1) + self.assertEqual(log_record.span_id, 2) + self.assertEqual(log_record.trace_flags, TraceFlags(TraceFlags.SAMPLED)) diff --git a/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs.py b/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs.py index 94d08cc09e0..5681638fff9 100644 --- a/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs.py +++ b/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs.py @@ -3,7 +3,7 @@ import pytest -from opentelemetry._logs import SeverityNumber +from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, @@ -11,6 +11,7 @@ SimpleLogRecordProcessor, ) from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider resource = Resource( { @@ -70,3 +71,60 @@ def benchmark_get_logger(): ) benchmark(benchmark_get_logger) + + +def test_emit_with_exception(benchmark): + try: + raise ValueError("benchmark exception") + except ValueError as exc: + exception = exc + + def benchmark_emit(): + simple_logger.emit( + severity_number=SeverityNumber.ERROR, + body="benchmark log message", + # pylint: disable-next=used-before-assignment + exception=exception, + ) + + benchmark(benchmark_emit) + + +def test_emit_in_active_span(benchmark): + tracer = TracerProvider(resource=resource, shutdown_on_exit=False).get_tracer("benchmark_tracer") + + def benchmark_emit(): + simple_logger.emit( + severity_number=SeverityNumber.INFO, + body="benchmark log message", + event_name="test.event", + ) + + with tracer.start_as_current_span("benchmark_span"): + benchmark(benchmark_emit) + + +@pytest.mark.parametrize("style", ["kwargs", "log_record"]) +def test_emit_log_record_vs_kwargs(benchmark, style): + benchmark.group = "emit-log-record-vs-kwargs" + attributes = {f"key{i}": f"value{i}" for i in range(5)} + + def benchmark_emit_kwargs(): + simple_logger.emit( + severity_number=SeverityNumber.INFO, + body="benchmark log message", + attributes=attributes, + event_name="test.event", + ) + + def benchmark_emit_log_record(): + simple_logger.emit( + LogRecord( + severity_number=SeverityNumber.INFO, + body="benchmark log message", + attributes=attributes, + event_name="test.event", + ) + ) + + benchmark(benchmark_emit_kwargs if style == "kwargs" else benchmark_emit_log_record) diff --git a/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs_enabled.py b/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs_enabled.py new file mode 100644 index 00000000000..38805184ac3 --- /dev/null +++ b/opentelemetry-sdk/benchmarks/logs/test_benchmark_logs_enabled.py @@ -0,0 +1,185 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import gc +import os +import tracemalloc +from unittest import mock + +import pytest + +from opentelemetry._logs import LogRecord, SeverityNumber +from opentelemetry._logs._internal import ProxyLogger +from opentelemetry.sdk._logs import LoggerProvider, LogRecordProcessor +from opentelemetry.sdk._logs._internal import _disable_logger_configurator +from opentelemetry.sdk.environment_variables import OTEL_SDK_DISABLED + + +class _SeverityFilteringProcessor(LogRecordProcessor): + """Processor that only accepts records at or above ``min_severity``. + + ``on_emit`` does no export work so the benchmarks measure the SDK + overhead of creating and dispatching the record. + """ + + def __init__(self, min_severity): + self._min_severity = min_severity + + def on_emit(self, log_record): + pass + + def enabled( + self, + *, + context=None, + instrumentation_scope=None, + severity_number=None, + event_name=None, + ): + return severity_number is None or severity_number.value >= self._min_severity.value + + def shutdown(self): + pass + + def force_flush(self, timeout_millis=30000): + return True + + +def _make_disabled_logger(mechanism): + """Returns a logger for which an INFO record is disabled.""" + if mechanism == "logger_config": + provider = LoggerProvider( + shutdown_on_exit=False, + _logger_configurator=_disable_logger_configurator, + ) + provider.add_log_record_processor(_SeverityFilteringProcessor(SeverityNumber.INFO)) + return provider.get_logger("bench") + if mechanism == "processor_filter": + provider = LoggerProvider(shutdown_on_exit=False) + provider.add_log_record_processor(_SeverityFilteringProcessor(SeverityNumber.WARN)) + return provider.get_logger("bench") + if mechanism == "no_processors": + return LoggerProvider(shutdown_on_exit=False).get_logger("bench") + if mechanism == "sdk_disabled": + with mock.patch.dict(os.environ, {OTEL_SDK_DISABLED: "true"}): + provider = LoggerProvider(shutdown_on_exit=False) + return provider.get_logger("bench") + if mechanism == "api_proxy_noop": + return ProxyLogger("bench") + raise ValueError(f"unknown mechanism: {mechanism}") + + +def _emit_unguarded(logger, index): + logger.emit( + severity_number=SeverityNumber.INFO, + body=f"benchmark log message {index}", + attributes={ + "http.request.method": "GET", + "http.route": "/users/{id}", + "http.response.status_code": 200, + "user.id": index, + "server.address": "example.com", + }, + event_name="test.event", + ) + + +def _emit_guarded(logger, index): + if logger.enabled(severity_number=SeverityNumber.INFO, event_name="test.event"): + _emit_unguarded(logger, index) + + +def _emit_log_record_unguarded(logger, index): + logger.emit( + LogRecord( + severity_number=SeverityNumber.INFO, + body=f"benchmark log message {index}", + attributes={ + "http.request.method": "GET", + "http.route": "/users/{id}", + "http.response.status_code": 200, + "user.id": index, + "server.address": "example.com", + }, + event_name="test.event", + ) + ) + + +def _emit_log_record_guarded(logger, index): + if logger.enabled(severity_number=SeverityNumber.INFO, event_name="test.event"): + logger.emit( + LogRecord( + severity_number=SeverityNumber.INFO, + body=f"benchmark log message {index}", + attributes={ + "http.request.method": "GET", + "http.route": "/users/{id}", + "http.response.status_code": 200, + "user.id": index, + "server.address": "example.com", + }, + event_name="test.event", + ) + ) + + +_EMIT_FUNCTIONS = { + ("kwargs", False): _emit_unguarded, + ("kwargs", True): _emit_guarded, + ("log_record", False): _emit_log_record_unguarded, + ("log_record", True): _emit_log_record_guarded, +} + + +def _measure_allocations(benchmark, emit, logger): + for index in range(100): + emit(logger, index) + + peaks = [] + for index in range(200): + tracemalloc.start() + emit(logger, index) + _, peak = tracemalloc.get_traced_memory() + tracemalloc.stop() + peaks.append(peak) + benchmark.extra_info["mean_alloc_bytes"] = sum(peaks) / len(peaks) + + # Measure gen-0 collections before/after emitting 10,000 log records + gc.collect() + collections_before = gc.get_stats()[0]["collections"] + for index in range(10_000): + emit(logger, index) + benchmark.extra_info["gc_gen0_collections_per_10k"] = gc.get_stats()[0]["collections"] - collections_before + + +@pytest.mark.parametrize("guarded", [False, True], ids=["unguarded", "guarded"]) +@pytest.mark.parametrize("style", ["kwargs", "log_record"]) +@pytest.mark.parametrize( + "mechanism", + [ + "logger_config", + "processor_filter", + "no_processors", + "sdk_disabled", + "api_proxy_noop", + ], +) +def test_disabled_emit(benchmark, mechanism, style, guarded): + benchmark.group = f"disabled-{mechanism}-{style}" + logger = _make_disabled_logger(mechanism) + emit = _EMIT_FUNCTIONS[(style, guarded)] + _measure_allocations(benchmark, emit, logger) + + benchmark(emit, logger, 42) + + +@pytest.mark.parametrize("guarded", [False, True], ids=["unguarded", "guarded"]) +def test_enabled_emit(benchmark, guarded): + benchmark.group = "enabled-guard-overhead" + provider = LoggerProvider(shutdown_on_exit=False) + provider.add_log_record_processor(_SeverityFilteringProcessor(SeverityNumber.INFO)) + logger = provider.get_logger("bench") + emit = _emit_guarded if guarded else _emit_unguarded + + benchmark(emit, logger, 42) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 5b543d01e44..9a0aa4cbd9e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -225,7 +225,7 @@ class LogLimits(LogRecordLimits): pass -@dataclass(frozen=True) +@dataclass(frozen=True, slots=True) class ReadableLogRecord: """Readable LogRecord should be kept exactly in-sync with ReadWriteLogRecord, only difference is the frozen=True param.""" @@ -269,7 +269,7 @@ def to_json(self, indent: int | None = 4) -> str: ) -@dataclass +@dataclass(slots=True) class ReadWriteLogRecord: """A ReadWriteLogRecord instance represents an event being logged. ReadWriteLogRecord instances are created and emitted via `Logger` @@ -300,7 +300,12 @@ def __post_init__(self): def __eq__(self, other: object) -> bool: if not isinstance(other, ReadWriteLogRecord): return NotImplemented - return self.__dict__ == other.__dict__ + return ( + self.log_record == other.log_record + and self.resource == other.resource + and self.instrumentation_scope == other.instrumentation_scope + and self.limits == other.limits + ) @property def dropped_attributes(self) -> int: @@ -435,16 +440,15 @@ def enabled( severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: - processors = self._log_record_processors - return any( - lp.enabled( + for lp in self._log_record_processors: + if lp.enabled( context=context, instrumentation_scope=instrumentation_scope, severity_number=severity_number, event_name=event_name, - ) - for lp in processors - ) + ): + return True + return False def shutdown(self) -> None: """Shutdown the log processors one by one""" @@ -524,16 +528,15 @@ def enabled( severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: - processors = self._log_record_processors - return any( - lp.enabled( + for lp in self._log_record_processors: + if lp.enabled( context=context, instrumentation_scope=instrumentation_scope, severity_number=severity_number, event_name=event_name, - ) - for lp in processors - ) + ): + return True + return False def shutdown(self) -> None: self._submit_and_wait(lambda lp: lp.shutdown) @@ -758,9 +761,6 @@ def __init__( self._logger_config = _logger_config self._log_record_limits = log_record_limits or LogRecordLimits() - def _is_enabled(self) -> bool: - return self._logger_config.is_enabled - def enabled( self, *, @@ -768,7 +768,7 @@ def enabled( severity_number: SeverityNumber | None = None, event_name: str | None = None, ) -> bool: - if not self._is_enabled(): + if not self._logger_config.is_enabled: return False return self._multi_log_record_processor.enabled( context=context, @@ -809,7 +809,28 @@ def emit( """Emits the :class:`ReadWriteLogRecord` by setting instrumentation scope and forwarding to the processor. """ - if not self._is_enabled(): + if not self._logger_config.is_enabled: + return + self._logger_metrics.emit_log() + instrumentation_scope = self._instrumentation_scope + if record is not None: + if isinstance(record, ReadWriteLogRecord): + instrumentation_scope = record.instrumentation_scope + api_record = record.log_record + else: + api_record = record + context = api_record.context + severity_number = api_record.severity_number + event_name = api_record.event_name + else: + context = context or get_current() + # Skip building the record when no processor would accept it. + if not self._multi_log_record_processor.enabled( + context=context, + instrumentation_scope=instrumentation_scope, + severity_number=severity_number, + event_name=event_name, + ): return # If a record is provided, use it directly if record is not None: @@ -847,7 +868,6 @@ def emit( limits=self._log_record_limits, ) - self._logger_metrics.emit_log() self._multi_log_record_processor.on_emit(writable_record) diff --git a/opentelemetry-sdk/tests/logs/test_log_record.py b/opentelemetry-sdk/tests/logs/test_log_record.py index 78f24704b48..7eab1712b0b 100644 --- a/opentelemetry-sdk/tests/logs/test_log_record.py +++ b/opentelemetry-sdk/tests/logs/test_log_record.py @@ -209,3 +209,19 @@ def test_log_record_from_api_log_record(self): self.assertEqual(record.log_record.attributes, {"a": "b"}) self.assertEqual(record.log_record.event_name, "an.event") self.assertEqual(record.resource, resource) + + def test_read_write_log_record_equality(self): + log_record = LogRecord(body="a log line") + resource = Resource.create({"service.name": "foo"}) + limits = LogRecordLimits() + record = ReadWriteLogRecord(log_record=log_record, resource=resource, limits=limits) + + self.assertEqual( + record, + ReadWriteLogRecord(log_record=log_record, resource=resource, limits=limits), + ) + self.assertNotEqual( + record, + ReadWriteLogRecord(log_record=LogRecord(body="other"), resource=resource, limits=limits), + ) + self.assertNotEqual(record, object()) diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index 38e38375a58..032e0c28b32 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -13,7 +13,7 @@ from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.attributes import BoundedAttributes -from opentelemetry.context import get_current +from opentelemetry.context import attach, detach, get_current, set_value from opentelemetry.sdk._logs import ( Logger, LoggerProvider, @@ -174,13 +174,13 @@ def test_default_logger_configurator(self): provider = LoggerProvider() logger = provider.get_logger("module_name", "1.0", "schema_url") other_logger = provider.get_logger("other_module_name", "1.0", "schema_url") - self.assertTrue(logger._is_enabled()) - self.assertTrue(other_logger._is_enabled()) + self.assertTrue(logger._logger_config.is_enabled) + self.assertTrue(other_logger._logger_config.is_enabled) def test_logger_provider_with_disabled_configurator(self): provider = LoggerProvider(_logger_configurator=_disable_logger_configurator) logger = provider.get_logger("test") - self.assertFalse(logger._is_enabled()) + self.assertFalse(logger._logger_config.is_enabled) def test_logger_provider_with_custom_configurator(self): def configurator(scope): @@ -191,22 +191,22 @@ def configurator(scope): provider = LoggerProvider(_logger_configurator=configurator) enabled = provider.get_logger("enabled_logger") disabled = provider.get_logger("disabled_logger") - self.assertTrue(enabled._is_enabled()) - self.assertFalse(disabled._is_enabled()) + self.assertTrue(enabled._logger_config.is_enabled) + self.assertFalse(disabled._logger_config.is_enabled) def test_set_logger_configurator_updates_existing_loggers(self): provider = LoggerProvider() logger = provider.get_logger("test") - self.assertTrue(logger._is_enabled()) + self.assertTrue(logger._logger_config.is_enabled) provider._set_logger_configurator(logger_configurator=_disable_logger_configurator) - self.assertFalse(logger._is_enabled()) + self.assertFalse(logger._logger_config.is_enabled) def test_set_logger_configurator_affects_new_loggers(self): provider = LoggerProvider() provider._set_logger_configurator(logger_configurator=_disable_logger_configurator) logger = provider.get_logger("new_logger") - self.assertFalse(logger._is_enabled()) + self.assertFalse(logger._logger_config.is_enabled) # pylint: disable-next=no-self-use def test_disabled_logger_skips_emit(self): @@ -235,13 +235,13 @@ def test_rule_based_logger_configurator(self): logger = provider.get_logger("module_name", "1.0", "schema_url") other_logger = provider.get_logger("other_module_name", "1.0", "schema_url") - self.assertTrue(logger._is_enabled()) - self.assertTrue(other_logger._is_enabled()) + self.assertTrue(logger._logger_config.is_enabled) + self.assertTrue(other_logger._logger_config.is_enabled) provider._set_logger_configurator(logger_configurator=configurator) - self.assertTrue(logger._is_enabled()) - self.assertFalse(other_logger._is_enabled()) + self.assertTrue(logger._logger_config.is_enabled) + self.assertFalse(other_logger._logger_config.is_enabled) def test_rule_based_logger_configurator_default_when_rules_dont_match( self, @@ -258,13 +258,13 @@ def test_rule_based_logger_configurator_default_when_rules_dont_match( logger = provider.get_logger("module_name", "1.0", "schema_url") other_logger = provider.get_logger("other_module_name", "1.0", "schema_url") - self.assertTrue(logger._is_enabled()) - self.assertTrue(other_logger._is_enabled()) + self.assertTrue(logger._logger_config.is_enabled) + self.assertTrue(other_logger._logger_config.is_enabled) provider._set_logger_configurator(logger_configurator=configurator) - self.assertFalse(logger._is_enabled()) - self.assertTrue(other_logger._is_enabled()) + self.assertFalse(logger._logger_config.is_enabled) + self.assertTrue(other_logger._logger_config.is_enabled) def test_rule_based_configurator_first_match_wins(self): disabled_config = _LoggerConfig(is_enabled=False) @@ -540,3 +540,105 @@ def test_enabled_one_processor_enabled_returns_true(self): provider.add_log_record_processor(p2) logger = provider.get_logger("test") self.assertTrue(logger.enabled()) + + def test_emit_skipped_when_all_processors_disabled(self): # pylint: disable=no-self-use + provider = LoggerProvider() + p1 = Mock() + p1.enabled.return_value = False + p2 = Mock() + p2.enabled.return_value = False + provider.add_log_record_processor(p1) + provider.add_log_record_processor(p2) + logger = provider.get_logger("test") + + logger.emit(body="a log line") + logger.emit(LogRecord(body="a log line")) + + p1.on_emit.assert_not_called() + p2.on_emit.assert_not_called() + + def test_emit_forwarded_when_one_processor_enabled(self): # pylint: disable=no-self-use + provider = LoggerProvider() + p1 = Mock() + p1.enabled.return_value = False + p2 = Mock() + p2.enabled.return_value = True + provider.add_log_record_processor(p1) + provider.add_log_record_processor(p2) + logger = provider.get_logger("test") + + logger.emit(body="a log line") + + p1.on_emit.assert_called_once() + p2.on_emit.assert_called_once() + + def test_emit_passes_args_to_processor_enabled(self): + provider = LoggerProvider() + processor_mock = Mock() + processor_mock.enabled.return_value = False + provider.add_log_record_processor(processor_mock) + logger = provider.get_logger("test") + context = get_current() + log_record = LogRecord( + context=context, + severity_number=SeverityNumber.WARN, + event_name="my.event", + ) + + other_scope = InstrumentationScope("other") + + cases = { + "kwargs": ( + lambda: logger.emit( + context=context, + severity_number=SeverityNumber.WARN, + event_name="my.event", + ), + logger._instrumentation_scope, + ), + "log_record": (lambda: logger.emit(log_record), logger._instrumentation_scope), + "read_write_log_record": ( + lambda: logger.emit( + ReadWriteLogRecord( + log_record=log_record, + instrumentation_scope=logger._instrumentation_scope, + ) + ), + logger._instrumentation_scope, + ), + "read_write_log_record_other_scope": ( + lambda: logger.emit(ReadWriteLogRecord(log_record=log_record, instrumentation_scope=other_scope)), + other_scope, + ), + } + for name, (emit, expected_scope) in cases.items(): + with self.subTest(name): + processor_mock.reset_mock() + emit() + processor_mock.enabled.assert_called_once_with( + context=context, + instrumentation_scope=expected_scope, + severity_number=SeverityNumber.WARN, + event_name="my.event", + ) + + def test_emit_kwargs_passes_current_context_to_processor_enabled(self): # pylint: disable=no-self-use + provider = LoggerProvider() + processor_mock = Mock() + processor_mock.enabled.return_value = False + provider.add_log_record_processor(processor_mock) + logger = provider.get_logger("test") + + token = attach(set_value("key", "value")) + try: + logger.emit(body="a log line") + expected_context = get_current() + finally: + detach(token) + + processor_mock.enabled.assert_called_once_with( + context=expected_context, + instrumentation_scope=logger._instrumentation_scope, + severity_number=None, + event_name=None, + ) diff --git a/opentelemetry-sdk/tests/logs/test_sdk_metrics.py b/opentelemetry-sdk/tests/logs/test_sdk_metrics.py index 8191039f847..78814220bb7 100644 --- a/opentelemetry-sdk/tests/logs/test_sdk_metrics.py +++ b/opentelemetry-sdk/tests/logs/test_sdk_metrics.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 from unittest import TestCase -from unittest.mock import patch +from unittest.mock import Mock, patch from opentelemetry.sdk._logs import LoggerProvider from opentelemetry.sdk.environment_variables import ( @@ -46,6 +46,22 @@ def test_create_logs(self): {}, ) + def test_create_logs_counted_when_processors_disabled(self): + logger_provider = LoggerProvider(meter_provider=self.meter_provider) + processor = Mock() + processor.enabled.return_value = False + logger_provider.add_log_record_processor(processor) + logger = logger_provider.get_logger("test") + + logger.emit(body="log1") + + processor.on_emit.assert_not_called() + self.assert_created_logs( + self.metric_reader.get_metrics_data(), + 1, + {}, + ) + class TestLoggerProviderMetricsDisabled(TestCase): def test_disabled_by_default(self):