diff --git a/CHANGELOG.md b/CHANGELOG.md index 478eeaf6f08..9130b344751 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4958](https://github.com/open-telemetry/opentelemetry-python/pull/4958)) - `opentelemetry-sdk`: fix type annotations on `MetricReader` and related types ([#4938](https://github.com/open-telemetry/opentelemetry-python/pull/4938/)) +- Implement log creation metric + ([#4935](https://github.com/open-telemetry/opentelemetry-python/pull/4935)) - `opentelemetry-sdk`: upgrade vendored OTel configuration schema from v1.0.0-rc.3 to v1.0.0 ([#4965](https://github.com/open-telemetry/opentelemetry-python/pull/4965)) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 442677fe4da..9029f867a7e 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -42,6 +42,8 @@ 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._logs._internal._logger_metrics import LoggerMetrics from opentelemetry.sdk.environment_variables import ( OTEL_ATTRIBUTE_COUNT_LIMIT, OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT, @@ -638,6 +640,8 @@ def __init__( ConcurrentMultiLogRecordProcessor, ], instrumentation_scope: InstrumentationScope, + *, + logger_metrics: LoggerMetrics, ): super().__init__( instrumentation_scope.name, @@ -648,6 +652,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): @@ -700,6 +705,7 @@ def emit( instrumentation_scope=self._instrumentation_scope, ) + self._logger_metrics.emit_log() self._multi_log_record_processor.on_emit(writable_record) @@ -711,6 +717,8 @@ def __init__( multi_log_record_processor: SynchronousMultiLogRecordProcessor | ConcurrentMultiLogRecordProcessor | None = None, + *, + meter_provider: MeterProvider | None = None, ): if resource is None: self._resource = Resource.create({}) @@ -719,6 +727,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 @@ -747,6 +758,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 00000000000..92a4c76a450 --- /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/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 37e4db77ec8..76219156ee4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -64,6 +64,7 @@ ) from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import sampling +from opentelemetry.sdk.trace._tracer_metrics import TracerMetrics from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator from opentelemetry.sdk.util import BoundedList from opentelemetry.sdk.util.instrumentation import ( @@ -81,8 +82,6 @@ from opentelemetry.util import types from opentelemetry.util._decorator import _agnosticcontextmanager -from ._tracer_metrics import TracerMetrics - logger = logging.getLogger(__name__) _DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128 diff --git a/opentelemetry-sdk/tests/logs/test_logs.py b/opentelemetry-sdk/tests/logs/test_logs.py index 70811260ae4..6a9c95685cd 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 00000000000..5a0e18c4fb9 --- /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, + {}, + )