diff --git a/CHANGELOG.md b/CHANGELOG.md index f3b925076a..d6a4c77ced 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4799](https://github.com/open-telemetry/opentelemetry-python/pull/4799)) and ([#4867](https://github.com/open-telemetry/opentelemetry-python/pull/4867)). - Implement span start/end metrics ([#4880](https://github.com/open-telemetry/opentelemetry-python/pull/4880)) +- Implement log creation metric + ([#4935](https://github.com/open-telemetry/opentelemetry-python/pull/4935)) - Add environment variable carriers to API ([#4609](https://github.com/open-telemetry/opentelemetry-python/pull/4609)) - Add experimental composable rule based sampler diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index d775dd4455..4bb9667800 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -42,6 +42,7 @@ from opentelemetry.attributes import _VALID_ANY_VALUE_TYPES, BoundedAttributes from opentelemetry.context import get_current from opentelemetry.context.context import Context +from opentelemetry.metrics import MeterProvider, get_meter_provider from opentelemetry.sdk.environment_variables import ( OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, @@ -58,6 +59,8 @@ ) from opentelemetry.util.types import AnyValue, _ExtendedAttributes +from ._logger_metrics import LoggerMetrics + _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128 _ENV_VALUE_UNSET = "" @@ -600,6 +603,8 @@ def __init__( ConcurrentMultiLogRecordProcessor, ], instrumentation_scope: InstrumentationScope, + *, + logger_metrics: LoggerMetrics, ): super().__init__( instrumentation_scope.name, @@ -610,6 +615,7 @@ def __init__( self._resource = resource self._multi_log_record_processor = multi_log_record_processor self._instrumentation_scope = instrumentation_scope + self._logger_metrics = logger_metrics @property def resource(self): @@ -662,6 +668,7 @@ def emit( instrumentation_scope=self._instrumentation_scope, ) + self._logger_metrics.emit_log() self._multi_log_record_processor.on_emit(writable_record) @@ -673,6 +680,8 @@ def __init__( multi_log_record_processor: SynchronousMultiLogRecordProcessor | ConcurrentMultiLogRecordProcessor | None = None, + *, + meter_provider: MeterProvider | None = None, ): if resource is None: self._resource = Resource.create({}) @@ -681,6 +690,9 @@ def __init__( self._multi_log_record_processor = ( multi_log_record_processor or SynchronousMultiLogRecordProcessor() ) + self._logger_metrics = LoggerMetrics( + meter_provider or get_meter_provider() + ) disabled = environ.get(OTEL_SDK_DISABLED, "") self._disabled = disabled.lower().strip() == "true" self._at_exit_handler = None @@ -709,6 +721,7 @@ def _get_logger_no_cache( schema_url, attributes, ), + logger_metrics=self._logger_metrics, ) def _get_logger_cached( diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/_logger_metrics.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/_logger_metrics.py new file mode 100644 index 0000000000..92a4c76a45 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/_logger_metrics.py @@ -0,0 +1,27 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from opentelemetry import metrics as metrics_api +from opentelemetry.semconv._incubating.metrics.otel_metrics import ( + create_otel_sdk_log_created, +) + + +class LoggerMetrics: + def __init__(self, meter_provider: metrics_api.MeterProvider) -> None: + meter = meter_provider.get_meter("opentelemetry-sdk") + self._created_logs = create_otel_sdk_log_created(meter) + + def emit_log(self) -> None: + self._created_logs.add(1) 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 f12b9dd8a2..ddae0de7dd 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/export/__init__.py @@ -116,9 +116,9 @@ class ConsoleLogRecordExporter(LogRecordExporter): def __init__( self, out: IO = sys.stdout, - formatter: Callable[ - [ReadableLogRecord], str - ] = lambda record: record.to_json() + linesep, + formatter: Callable[[ReadableLogRecord], str] = lambda record: ( + record.to_json() + linesep + ), ): self.out = out self.formatter = formatter diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py index cdbad3e343..b3ff93ea35 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/metrics/_internal/export/__init__.py @@ -144,9 +144,9 @@ class ConsoleMetricExporter(MetricExporter): def __init__( self, out: IO = stdout, - formatter: Callable[ - [MetricsData], str - ] = lambda metrics_data: metrics_data.to_json() + linesep, + formatter: Callable[[MetricsData], str] = lambda metrics_data: ( + metrics_data.to_json() + linesep + ), preferred_temporality: dict[type, AggregationTemporality] | None = None, preferred_aggregation: dict[ diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py index a9108b7337..d853dfd6c4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/export/__init__.py @@ -300,9 +300,9 @@ def __init__( self, service_name: str | None = None, out: typing.IO = sys.stdout, - formatter: typing.Callable[ - [ReadableSpan], str - ] = lambda span: span.to_json() + linesep, + formatter: typing.Callable[[ReadableSpan], str] = lambda span: ( + span.to_json() + linesep + ), ): self.out = out self.formatter = formatter diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index 70811260ae..6a9c95685c 100644 --- a/opentelemetry-sdk/tests/logs/test_logs.py +++ b/opentelemetry-sdk/tests/logs/test_logs.py @@ -19,12 +19,14 @@ from opentelemetry._logs import LogRecord, SeverityNumber from opentelemetry.context import get_current +from opentelemetry.metrics import NoOpMeterProvider from opentelemetry.sdk._logs import ( Logger, LoggerProvider, ReadableLogRecord, ) from opentelemetry.sdk._logs._internal import ( + LoggerMetrics, NoOpLogger, SynchronousMultiLogRecordProcessor, ) @@ -148,6 +150,7 @@ def _get_logger(): "schema_url", {"an": "attribute"}, ), + logger_metrics=LoggerMetrics(NoOpMeterProvider()), ) return logger, log_record_processor_mock diff --git a/opentelemetry-sdk/tests/logs/test_sdk_metrics.py b/opentelemetry-sdk/tests/logs/test_sdk_metrics.py new file mode 100644 index 0000000000..5a0e18c4fb --- /dev/null +++ b/opentelemetry-sdk/tests/logs/test_sdk_metrics.py @@ -0,0 +1,59 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from unittest import TestCase + +from opentelemetry.sdk._logs import LoggerProvider +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.metrics.export import InMemoryMetricReader + + +class TestLoggerProviderMetrics(TestCase): + def setUp(self): + self.metric_reader = InMemoryMetricReader() + self.meter_provider = MeterProvider( + metric_readers=[self.metric_reader] + ) + + def tearDown(self): + self.meter_provider.shutdown() + + def assert_created_logs(self, metric_data, value, attrs): + metrics = metric_data.resource_metrics[0].scope_metrics[0].metrics + created_logs_metric = next( + (m for m in metrics if m.name == "otel.sdk.log.created"), None + ) + self.assertIsNotNone(created_logs_metric) + self.assertEqual(created_logs_metric.data.data_points[0].value, value) + self.assertDictEqual( + created_logs_metric.data.data_points[0].attributes, attrs + ) + + def test_create_logs(self): + logger_provider = LoggerProvider(meter_provider=self.meter_provider) + logger = logger_provider.get_logger("test") + logger.emit(body="log1") + metric_data = self.metric_reader.get_metrics_data() + self.assert_created_logs( + metric_data, + 1, + {}, + ) + logger.emit(body="log2") + metric_data = self.metric_reader.get_metrics_data() + self.assert_created_logs( + metric_data, + 2, + {}, + )