Skip to content
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -58,6 +59,8 @@
)
from opentelemetry.util.types import AnyValue, _ExtendedAttributes

from ._logger_metrics import LoggerMetrics
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small nit, but not sure if we want to stick to the convention of using absolute paths.


_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128
_ENV_VALUE_UNSET = ""

Expand Down Expand Up @@ -600,6 +603,8 @@ def __init__(
ConcurrentMultiLogRecordProcessor,
],
instrumentation_scope: InstrumentationScope,
*,
logger_metrics: LoggerMetrics,
):
super().__init__(
instrumentation_scope.name,
Expand All @@ -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):
Expand Down Expand Up @@ -662,6 +668,7 @@ def emit(
instrumentation_scope=self._instrumentation_scope,
)

self._logger_metrics.emit_log()
Copy link
Contributor

@xrmx xrmx Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe call this on_emit as well? emit_log sounds like you are going to emit a new log more than it has been emitted.

self._multi_log_record_processor.on_emit(writable_record)


Expand All @@ -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({})
Expand All @@ -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
Expand Down Expand Up @@ -709,6 +721,7 @@ def _get_logger_no_cache(
schema_url,
attributes,
),
logger_metrics=self._logger_metrics,
)

def _get_logger_cached(
Expand Down
Original file line number Diff line number Diff line change
@@ -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:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we could make this a bit more descriptive, something like

LogMetricsObserver

Let me know what you think

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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
self._created_logs = create_otel_sdk_log_created(meter)
self._logs_created_counter = create_otel_sdk_log_created(meter)


def emit_log(self) -> None:
self._created_logs.add(1)
Original file line number Diff line number Diff line change
Expand Up @@ -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: (
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a newer ruff installed?

record.to_json() + linesep
),
):
self.out = out
self.formatter = formatter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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[
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions opentelemetry-sdk/tests/logs/test_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -148,6 +150,7 @@ def _get_logger():
"schema_url",
{"an": "attribute"},
),
logger_metrics=LoggerMetrics(NoOpMeterProvider()),
)
return logger, log_record_processor_mock

Expand Down
59 changes: 59 additions & 0 deletions opentelemetry-sdk/tests/logs/test_sdk_metrics.py
Original file line number Diff line number Diff line change
@@ -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,
{},
)
Loading