From eac92499f901a65e5117ed2d0ba9b887772bf28f Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Fri, 2 Oct 2026 22:47:38 -0400 Subject: [PATCH 1/2] exporters: stabilize logs signal --- README.md | 7 +- docs/examples/logs/README.rst | 7 - docs/examples/logs/example.py | 2 +- .../metrics/sdk-metrics/sdk_metrics.py | 2 +- .../multi_destination_logs.py | 4 +- docs/exporter/otlp/otlp.rst | 8 +- .../benchmarks/test_benchmark_log_encoder.py | 2 +- .../{_log_encoder => log_encoder}/__init__.py | 0 .../exporter/otlp/json/common/_log_encoder.py | 23 +- .../exporter/otlp/json/common/log_encoder.py | 8 + .../tests/test_deprecated_log_module.py | 58 ++++ .../tests/test_log_encoder.py | 2 +- .../tests/test_proto_json_compatibility.py | 4 +- .../pyproject.toml | 2 +- .../exporter/otlp/json/file/__init__.py | 6 +- .../exporter/otlp/json/file/_log_exporter.py | 83 ++---- .../exporter/otlp/json/file/log_exporter.py | 70 +++++ .../tests/test_deprecated_log_module.py | 60 +++++ .../tests/test_log_exporter.py | 11 +- .../pyproject.toml | 2 +- .../exporter/otlp/json/http/__init__.py | 6 +- .../exporter/otlp/json/http/_log_exporter.py | 147 ++--------- .../exporter/otlp/json/http/log_exporter.py | 134 ++++++++++ .../tests/test_deprecated_log_module.py | 60 +++++ .../tests/test_log_exporter.py | 17 +- .../{_log_encoder => log_encoder}/__init__.py | 0 .../otlp/proto/common/_log_encoder.py | 22 +- .../exporter/otlp/proto/common/log_encoder.py | 9 + .../tests/test_bytes_resource.py | 2 +- .../tests/test_deprecated_log_module.py | 58 ++++ .../tests/test_log_encoder.py | 2 +- .../pyproject.toml | 2 +- .../otlp/proto/grpc/_log_exporter/__init__.py | 137 ++-------- .../otlp/proto/grpc/log_exporter/__init__.py | 124 +++++++++ .../tests/logs/test_deprecated_log_module.py | 60 +++++ .../tests/logs/test_otlp_logs_exporter.py | 2 +- .../pyproject.toml | 2 +- .../otlp/proto/http/_log_exporter/__init__.py | 248 +++--------------- .../otlp/proto/http/log_exporter/__init__.py | 223 ++++++++++++++++ .../tests/test_deprecated_log_module.py | 69 +++++ .../tests/test_proto_log_exporter.py | 12 +- .../pyproject.toml | 2 +- .../tests/test_otlp.py | 2 +- .../configuration/_logger_provider.py | 6 +- .../tests/test_logger_provider.py | 20 +- rationale.md | 7 + .../otlpexporter/test_otlp_logs_functional.py | 8 +- .../tests/test_otlp_test_server.py | 2 +- 48 files changed, 1146 insertions(+), 598 deletions(-) rename exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/{_log_encoder => log_encoder}/__init__.py (100%) create mode 100644 exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/log_encoder.py create mode 100644 exporter/opentelemetry-exporter-otlp-json-common/tests/test_deprecated_log_module.py create mode 100644 exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file/log_exporter.py create mode 100644 exporter/opentelemetry-exporter-otlp-json-file/tests/test_deprecated_log_module.py create mode 100644 exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/log_exporter.py create mode 100644 exporter/opentelemetry-exporter-otlp-json-http/tests/test_deprecated_log_module.py rename exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/{_log_encoder => log_encoder}/__init__.py (100%) create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/log_encoder.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-common/tests/test_deprecated_log_module.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_deprecated_log_module.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/log_exporter/__init__.py create mode 100644 exporter/opentelemetry-exporter-otlp-proto-http/tests/test_deprecated_log_module.py diff --git a/README.md b/README.md index a7c799460f8..18e91ee57e4 100644 --- a/README.md +++ b/README.md @@ -14,15 +14,10 @@ See the [OpenTelemetry Instrumentation for Python](https://opentelemetry.io/docs | ------- | ------------ | ------- | | Traces | Stable | N/A | | Metrics | Stable | N/A | -| Logs | Development* | N/A | +| Logs | Stable | N/A | Project versioning information and stability guarantees can be found [here](./rationale.md#versioning-and-releasing). -***Breaking Changes** - -> [!IMPORTANT] -> We are working on stabilizing the Log signal which would require making deprecations and breaking changes. We will try to reduce the releases that may require an update to your code, especially for instrumentations or for SDK developers. - ## Getting started You can find the getting started guide for OpenTelemetry Python [here](https://opentelemetry.io/docs/languages/python/getting-started/). diff --git a/docs/examples/logs/README.rst b/docs/examples/logs/README.rst index 680d3092d24..ade1c9f666a 100644 --- a/docs/examples/logs/README.rst +++ b/docs/examples/logs/README.rst @@ -3,13 +3,6 @@ OpenTelemetry Logs SDK The source files of these examples are available :scm_web:`here `. -.. note:: - - ``LoggingHandler`` has moved from the SDK to the - ``opentelemetry-instrumentation-logging`` package in the contrib repo - and is deprecated in ``opentelemetry-sdk``. - - Installation ------------ diff --git a/docs/examples/logs/example.py b/docs/examples/logs/example.py index 20a5f2175a3..a06a127e082 100644 --- a/docs/examples/logs/example.py +++ b/docs/examples/logs/example.py @@ -4,7 +4,7 @@ import logging from opentelemetry import trace -from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( +from opentelemetry.exporter.otlp.proto.grpc.log_exporter import ( OTLPLogExporter, ) diff --git a/docs/examples/metrics/sdk-metrics/sdk_metrics.py b/docs/examples/metrics/sdk-metrics/sdk_metrics.py index 72dede5df93..18ae5e45a7e 100644 --- a/docs/examples/metrics/sdk-metrics/sdk_metrics.py +++ b/docs/examples/metrics/sdk-metrics/sdk_metrics.py @@ -5,7 +5,7 @@ import time from opentelemetry import logs, metrics, trace -from opentelemetry.exporter.otlp.proto.http._log_exporter import ( +from opentelemetry.exporter.otlp.proto.http.log_exporter import ( OTLPLogExporter, ) from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( diff --git a/docs/examples/multi-destination-exporting/multi_destination_logs.py b/docs/examples/multi-destination-exporting/multi_destination_logs.py index 0e4c2e81239..97fef64d2b4 100644 --- a/docs/examples/multi-destination-exporting/multi_destination_logs.py +++ b/docs/examples/multi-destination-exporting/multi_destination_logs.py @@ -9,10 +9,10 @@ import logging -from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( +from opentelemetry.exporter.otlp.proto.grpc.log_exporter import ( OTLPLogExporter as GrpcLogExporter, ) -from opentelemetry.exporter.otlp.proto.http._log_exporter import ( +from opentelemetry.exporter.otlp.proto.http.log_exporter import ( OTLPLogExporter as HttpLogExporter, ) diff --git a/docs/exporter/otlp/otlp.rst b/docs/exporter/otlp/otlp.rst index 343ec153895..35b4b688822 100644 --- a/docs/exporter/otlp/otlp.rst +++ b/docs/exporter/otlp/otlp.rst @@ -23,7 +23,7 @@ opentelemetry.exporter.otlp.proto.http :undoc-members: :show-inheritance: -.. automodule:: opentelemetry.exporter.otlp.proto.http._log_exporter +.. automodule:: opentelemetry.exporter.otlp.proto.http.log_exporter :members: :undoc-members: :show-inheritance: @@ -46,7 +46,7 @@ opentelemetry.exporter.otlp.proto.grpc :undoc-members: :show-inheritance: -.. automodule:: opentelemetry.exporter.otlp.proto.grpc._log_exporter +.. automodule:: opentelemetry.exporter.otlp.proto.grpc.log_exporter :members: :undoc-members: :show-inheritance: @@ -68,7 +68,7 @@ opentelemetry.exporter.otlp.json.file :undoc-members: :show-inheritance: -.. automodule:: opentelemetry.exporter.otlp.json.file._log_exporter +.. automodule:: opentelemetry.exporter.otlp.json.file.log_exporter :members: :undoc-members: :show-inheritance: @@ -90,7 +90,7 @@ opentelemetry.exporter.otlp.json.http :undoc-members: :show-inheritance: -.. automodule:: opentelemetry.exporter.otlp.json.http._log_exporter +.. automodule:: opentelemetry.exporter.otlp.json.http.log_exporter :members: :undoc-members: :show-inheritance: diff --git a/exporter/opentelemetry-exporter-otlp-json-common/benchmarks/test_benchmark_log_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/benchmarks/test_benchmark_log_encoder.py index 0343446f2b4..8d7e49a1431 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/benchmarks/test_benchmark_log_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/benchmarks/test_benchmark_log_encoder.py @@ -3,7 +3,7 @@ import pytest -from opentelemetry.exporter.otlp.json.common._log_encoder import encode_logs +from opentelemetry.exporter.otlp.json.common.log_encoder import encode_logs from opentelemetry.logs import SeverityNumber from tests import TIME, make_log, make_log_context diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/_log_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/log_encoder/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/_log_encoder/__init__.py rename to exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_internal/log_encoder/__init__.py diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_log_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_log_encoder.py index ebe6f660de0..9dc9dfbc539 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_log_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/_log_encoder.py @@ -1,8 +1,27 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +""" +Deprecated alias of :mod:`opentelemetry.exporter.otlp.json.common.log_encoder`. -from opentelemetry.exporter.otlp.json.common._internal._log_encoder import ( +.. deprecated:: 1.46.0 + This module will be removed in a future release. Use + :mod:`opentelemetry.exporter.otlp.json.common.log_encoder` instead. +""" + +import warnings + +from opentelemetry.exporter.otlp.json.common.log_encoder import ( encode_logs, ) -__all__ = ["encode_logs"] +warnings.warn( + "The opentelemetry.exporter.otlp.json.common._log_encoder module is deprecated since version 1.46.0 " + "and will be removed in a future release. " + "Use opentelemetry.exporter.otlp.json.common.log_encoder instead.", + DeprecationWarning, + stacklevel=2, +) + +__all__ = [ + "encode_logs", +] diff --git a/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/log_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/log_encoder.py new file mode 100644 index 00000000000..02863910820 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/src/opentelemetry/exporter/otlp/json/common/log_encoder.py @@ -0,0 +1,8 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry.exporter.otlp.json.common._internal.log_encoder import ( + encode_logs, +) + +__all__ = ["encode_logs"] diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_deprecated_log_module.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_deprecated_log_module.py new file mode 100644 index 00000000000..e8ddcb43b94 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_deprecated_log_module.py @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import importlib +import sys +import unittest +from importlib.metadata import EntryPoint + +import opentelemetry.exporter.otlp.json.common.log_encoder + +_DEPRECATED_MODULE = "opentelemetry.exporter.otlp.json.common._log_encoder" + + +def _unload_deprecated_module() -> None: + sys.modules.pop(_DEPRECATED_MODULE, None) + sys.modules["opentelemetry.exporter.otlp.json.common"].__dict__.pop("_log_encoder", None) + + +class TestDeprecatedLogModule(unittest.TestCase): + def setUp(self) -> None: + _unload_deprecated_module() + + def test_import_emits_deprecation_warning(self) -> None: + with self.assertWarns(DeprecationWarning) as context: + importlib.import_module(_DEPRECATED_MODULE) + message = str(context.warning) + self.assertIn("opentelemetry.exporter.otlp.json.common.log_encoder", message) + self.assertIn("will be removed in a future release", message) + + def test_deprecation_warning_points_at_importer(self) -> None: + # pylint: disable=import-outside-toplevel,unused-import + # A literal import statement is required here since importlib.import_module + # would attribute the warning to importlib itself. + with self.assertWarns(DeprecationWarning) as context: + from opentelemetry.exporter.otlp.json.common._log_encoder import encode_logs # noqa: F401, PLC0415 + self.assertEqual(context.filename, __file__) + + def test_reexports_are_identical(self) -> None: + with self.assertWarns(DeprecationWarning): + deprecated = importlib.import_module(_DEPRECATED_MODULE) + + self.assertEqual(deprecated.__all__, ["encode_logs"]) + for name in deprecated.__all__: + with self.subTest(name=name): + self.assertIs( + getattr(deprecated, name), getattr(opentelemetry.exporter.otlp.json.common.log_encoder, name) + ) + + def test_old_entry_point_target_resolves(self) -> None: + # Entry points and configuration may still reference the old module path. + entry_point = EntryPoint( + name="log_encoder", value="opentelemetry.exporter.otlp.json.common._log_encoder:encode_logs", group="test" + ) + with self.assertWarns(DeprecationWarning): + target = entry_point.load() + self.assertIs(target, opentelemetry.exporter.otlp.json.common.log_encoder.encode_logs) diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_log_encoder.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_log_encoder.py index a6cc81e86e9..d9bd0b4aca8 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_log_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_log_encoder.py @@ -9,7 +9,7 @@ _encode_trace_id, _encode_value, ) -from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs +from opentelemetry.exporter.otlp.proto.common.log_encoder import encode_logs from opentelemetry.logs import LogRecord, SeverityNumber from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, diff --git a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_proto_json_compatibility.py b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_proto_json_compatibility.py index a44371f6e5f..80c1457ef73 100644 --- a/exporter/opentelemetry-exporter-otlp-json-common/tests/test_proto_json_compatibility.py +++ b/exporter/opentelemetry-exporter-otlp-json-common/tests/test_proto_json_compatibility.py @@ -17,7 +17,7 @@ from google.protobuf.json_format import MessageToDict, ParseDict -from opentelemetry.exporter.otlp.json.common._log_encoder import ( +from opentelemetry.exporter.otlp.json.common.log_encoder import ( encode_logs as json_encode_logs, ) from opentelemetry.exporter.otlp.json.common.metrics_encoder import ( @@ -26,7 +26,7 @@ from opentelemetry.exporter.otlp.json.common.trace_encoder import ( encode_spans as json_encode_spans, ) -from opentelemetry.exporter.otlp.proto.common._log_encoder import ( +from opentelemetry.exporter.otlp.proto.common.log_encoder import ( encode_logs as proto_encode_logs, ) from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( diff --git a/exporter/opentelemetry-exporter-otlp-json-file/pyproject.toml b/exporter/opentelemetry-exporter-otlp-json-file/pyproject.toml index acb65d843ba..e1a504e9688 100644 --- a/exporter/opentelemetry-exporter-otlp-json-file/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-json-file/pyproject.toml @@ -38,7 +38,7 @@ otlp_json_file = "opentelemetry.exporter.otlp.json.file.trace_exporter:FileSpanE otlp_json_file = "opentelemetry.exporter.otlp.json.file.metric_exporter:FileMetricExporter" [project.entry-points.opentelemetry_logs_exporter] -otlp_json_file = "opentelemetry.exporter.otlp.json.file._log_exporter:FileLogExporter" +otlp_json_file = "opentelemetry.exporter.otlp.json.file.log_exporter:FileLogExporter" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-json-file" diff --git a/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file/__init__.py b/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file/__init__.py index ff60b9d8874..a480b95331d 100644 --- a/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file/__init__.py @@ -16,7 +16,7 @@ - :class:`~opentelemetry.exporter.otlp.json.file.trace_exporter.FileSpanExporter` - traces - :class:`~opentelemetry.exporter.otlp.json.file.metric_exporter.FileMetricExporter` - metrics -- :class:`~opentelemetry.exporter.otlp.json.file._log_exporter.FileLogExporter` - logs +- :class:`~opentelemetry.exporter.otlp.json.file.log_exporter.FileLogExporter` - logs Each exporter accepts a destination in one of three ways: @@ -60,6 +60,9 @@ --- """ +from opentelemetry.exporter.otlp.json.file.log_exporter import ( + FileLogExporter, +) from opentelemetry.exporter.otlp.json.file.metric_exporter import ( FileMetricExporter, ) @@ -68,6 +71,7 @@ ) __all__ = [ + "FileLogExporter", "FileMetricExporter", "FileSpanExporter", ] diff --git a/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file/_log_exporter.py b/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file/_log_exporter.py index 5ea8b078e31..2efab3b5a4e 100644 --- a/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file/_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file/_log_exporter.py @@ -1,70 +1,27 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +""" +Deprecated alias of :mod:`opentelemetry.exporter.otlp.json.file.log_exporter`. -import logging -import os -from collections.abc import Sequence -from typing import IO, Any, overload +.. deprecated:: 1.46.0 + This module will be removed in a future release. Use + :mod:`opentelemetry.exporter.otlp.json.file.log_exporter` instead. +""" -from opentelemetry.exporter.otlp.json.common._log_encoder import encode_logs -from opentelemetry.exporter.otlp.json.file._internal import _FileExporter -from opentelemetry.sdk._shared_internal import DuplicateFilter -from opentelemetry.sdk.logs import ReadableLogRecord -from opentelemetry.sdk.logs.export import ( - LogRecordExporter, - LogRecordExportResult, -) - -_logger = logging.getLogger(__name__) -_logger.addFilter(DuplicateFilter()) - - -def _encode_logs_to_dict( - batch: Sequence[ReadableLogRecord], -) -> dict[str, Any] | None: - data = encode_logs(batch) - return data.to_dict() if data.resource_logs else None - - -class FileLogExporter(LogRecordExporter): - @overload - def __init__( - self, - path: str | os.PathLike[str], - ) -> None: ... +import warnings - @overload - def __init__( - self, - *, - stream: IO[str], - ) -> None: ... - - @overload - def __init__( - self, - ) -> None: ... - - def __init__( - self, - path: str | os.PathLike[str] | None = None, - *, - stream: IO[str] | None = None, - ) -> None: - self._exporter: _FileExporter[Sequence[ReadableLogRecord]] = _FileExporter( - encode=_encode_logs_to_dict, - kind="logs", - logger=_logger, - path=path, - stream=stream, - ) - - def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: - return LogRecordExportResult.SUCCESS if self._exporter.export(batch) else LogRecordExportResult.FAILURE +from opentelemetry.exporter.otlp.json.file.log_exporter import ( + FileLogExporter, +) - def shutdown(self) -> None: - self._exporter.shutdown() +warnings.warn( + "The opentelemetry.exporter.otlp.json.file._log_exporter module is deprecated since version 1.46.0 " + "and will be removed in a future release. " + "Use opentelemetry.exporter.otlp.json.file.log_exporter instead.", + DeprecationWarning, + stacklevel=2, +) - # pylint: disable-next=no-self-use - def force_flush(self, timeout_millis: int = 10_000) -> bool: - return True +__all__ = [ + "FileLogExporter", +] diff --git a/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file/log_exporter.py b/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file/log_exporter.py new file mode 100644 index 00000000000..8ef5de9623c --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-file/src/opentelemetry/exporter/otlp/json/file/log_exporter.py @@ -0,0 +1,70 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import logging +import os +from collections.abc import Sequence +from typing import IO, Any, overload + +from opentelemetry.exporter.otlp.json.common.log_encoder import encode_logs +from opentelemetry.exporter.otlp.json.file._internal import _FileExporter +from opentelemetry.sdk._shared_internal import DuplicateFilter +from opentelemetry.sdk.logs import ReadableLogRecord +from opentelemetry.sdk.logs.export import ( + LogRecordExporter, + LogRecordExportResult, +) + +_logger = logging.getLogger(__name__) +_logger.addFilter(DuplicateFilter()) + + +def _encode_logs_to_dict( + batch: Sequence[ReadableLogRecord], +) -> dict[str, Any] | None: + data = encode_logs(batch) + return data.to_dict() if data.resource_logs else None + + +class FileLogExporter(LogRecordExporter): + @overload + def __init__( + self, + path: str | os.PathLike[str], + ) -> None: ... + + @overload + def __init__( + self, + *, + stream: IO[str], + ) -> None: ... + + @overload + def __init__( + self, + ) -> None: ... + + def __init__( + self, + path: str | os.PathLike[str] | None = None, + *, + stream: IO[str] | None = None, + ) -> None: + self._exporter: _FileExporter[Sequence[ReadableLogRecord]] = _FileExporter( + encode=_encode_logs_to_dict, + kind="logs", + logger=_logger, + path=path, + stream=stream, + ) + + def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: + return LogRecordExportResult.SUCCESS if self._exporter.export(batch) else LogRecordExportResult.FAILURE + + def shutdown(self) -> None: + self._exporter.shutdown() + + # pylint: disable-next=no-self-use + def force_flush(self, timeout_millis: int = 10_000) -> bool: + return True diff --git a/exporter/opentelemetry-exporter-otlp-json-file/tests/test_deprecated_log_module.py b/exporter/opentelemetry-exporter-otlp-json-file/tests/test_deprecated_log_module.py new file mode 100644 index 00000000000..532db622976 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-file/tests/test_deprecated_log_module.py @@ -0,0 +1,60 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import importlib +import sys +import unittest +from importlib.metadata import EntryPoint + +import opentelemetry.exporter.otlp.json.file.log_exporter + +_DEPRECATED_MODULE = "opentelemetry.exporter.otlp.json.file._log_exporter" + + +def _unload_deprecated_module() -> None: + sys.modules.pop(_DEPRECATED_MODULE, None) + sys.modules["opentelemetry.exporter.otlp.json.file"].__dict__.pop("_log_exporter", None) + + +class TestDeprecatedLogModule(unittest.TestCase): + def setUp(self) -> None: + _unload_deprecated_module() + + def test_import_emits_deprecation_warning(self) -> None: + with self.assertWarns(DeprecationWarning) as context: + importlib.import_module(_DEPRECATED_MODULE) + message = str(context.warning) + self.assertIn("opentelemetry.exporter.otlp.json.file.log_exporter", message) + self.assertIn("will be removed in a future release", message) + + def test_deprecation_warning_points_at_importer(self) -> None: + # pylint: disable=import-outside-toplevel,unused-import + # A literal import statement is required here since importlib.import_module + # would attribute the warning to importlib itself. + with self.assertWarns(DeprecationWarning) as context: + from opentelemetry.exporter.otlp.json.file._log_exporter import FileLogExporter # noqa: F401, PLC0415 + self.assertEqual(context.filename, __file__) + + def test_reexports_are_identical(self) -> None: + with self.assertWarns(DeprecationWarning): + deprecated = importlib.import_module(_DEPRECATED_MODULE) + + self.assertEqual(deprecated.__all__, ["FileLogExporter"]) + for name in deprecated.__all__: + with self.subTest(name=name): + self.assertIs( + getattr(deprecated, name), getattr(opentelemetry.exporter.otlp.json.file.log_exporter, name) + ) + + def test_old_entry_point_target_resolves(self) -> None: + # Entry points and configuration may still reference the old module path. + entry_point = EntryPoint( + name="log_exporter", + value="opentelemetry.exporter.otlp.json.file._log_exporter:FileLogExporter", + group="test", + ) + with self.assertWarns(DeprecationWarning): + target = entry_point.load() + self.assertIs(target, opentelemetry.exporter.otlp.json.file.log_exporter.FileLogExporter) diff --git a/exporter/opentelemetry-exporter-otlp-json-file/tests/test_log_exporter.py b/exporter/opentelemetry-exporter-otlp-json-file/tests/test_log_exporter.py index 5990b8e2226..d89b0740d9f 100644 --- a/exporter/opentelemetry-exporter-otlp-json-file/tests/test_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-json-file/tests/test_log_exporter.py @@ -10,9 +10,10 @@ import unittest from unittest.mock import Mock -from opentelemetry.exporter.otlp.json.common._log_encoder import encode_logs +from opentelemetry.exporter.otlp.json import file as otlp_json_file +from opentelemetry.exporter.otlp.json.common.log_encoder import encode_logs from opentelemetry.exporter.otlp.json.file._internal import _format_line -from opentelemetry.exporter.otlp.json.file._log_exporter import ( +from opentelemetry.exporter.otlp.json.file.log_exporter import ( FileLogExporter, ) from opentelemetry.logs import LogRecord, SeverityNumber @@ -29,7 +30,7 @@ from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.util.instrumentation import InstrumentationScope -_LOGGER_NAME = "opentelemetry.exporter.otlp.json.file._log_exporter" +_LOGGER_NAME = "opentelemetry.exporter.otlp.json.file.log_exporter" def _make_log_record( @@ -52,6 +53,10 @@ def setUp(self): self._stream = io.StringIO() self._exporter = FileLogExporter(stream=self._stream) + def test_exported_at_package_level(self): + self.assertIs(otlp_json_file.FileLogExporter, FileLogExporter) + self.assertIn("FileLogExporter", otlp_json_file.__all__) + def test_export_empty_sequence(self): result = self._exporter.export([]) self.assertEqual(result, LogRecordExportResult.SUCCESS) diff --git a/exporter/opentelemetry-exporter-otlp-json-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-json-http/pyproject.toml index a0e65c76351..6581cb5cb3a 100644 --- a/exporter/opentelemetry-exporter-otlp-json-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-json-http/pyproject.toml @@ -40,7 +40,7 @@ otlp_json_http = "opentelemetry.exporter.otlp.json.http.trace_exporter:OTLPSpanE otlp_json_http = "opentelemetry.exporter.otlp.json.http.metric_exporter:OTLPMetricExporter" [project.entry-points.opentelemetry_logs_exporter] -otlp_json_http = "opentelemetry.exporter.otlp.json.http._log_exporter:OTLPLogExporter" +otlp_json_http = "opentelemetry.exporter.otlp.json.http.log_exporter:OTLPLogExporter" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-json-http" diff --git a/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/__init__.py b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/__init__.py index 1a39681f0f4..443e701181e 100644 --- a/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/__init__.py @@ -16,7 +16,7 @@ - :class:`~opentelemetry.exporter.otlp.json.http.trace_exporter.OTLPSpanExporter` - traces - :class:`~opentelemetry.exporter.otlp.json.http.metric_exporter.OTLPMetricExporter` - metrics -- :class:`~opentelemetry.exporter.otlp.json.http._log_exporter.OTLPLogExporter` - logs +- :class:`~opentelemetry.exporter.otlp.json.http.log_exporter.OTLPLogExporter` - logs You can configure each exporter with the following environment variables, using the appropriate per-signal prefix (``TRACES``, ``METRICS``, or @@ -66,6 +66,9 @@ --- """ +from opentelemetry.exporter.otlp.json.http.log_exporter import ( + OTLPLogExporter, +) from opentelemetry.exporter.otlp.json.http.metric_exporter import ( OTLPMetricExporter, ) @@ -74,6 +77,7 @@ ) __all__ = [ + "OTLPLogExporter", "OTLPMetricExporter", "OTLPSpanExporter", ] diff --git a/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/_log_exporter.py b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/_log_exporter.py index a27f63fed10..3829bfedfe3 100644 --- a/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/_log_exporter.py @@ -1,134 +1,27 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +""" +Deprecated alias of :mod:`opentelemetry.exporter.otlp.json.http.log_exporter`. -import logging -from collections.abc import Mapping, Sequence -from typing import overload +.. deprecated:: 1.46.0 + This module will be removed in a future release. Use + :mod:`opentelemetry.exporter.otlp.json.http.log_exporter` instead. +""" -from opentelemetry.exporter.http.transport._base import BaseHTTPTransport -from opentelemetry.exporter.otlp.common.http import ( - Compression, - _OTLPHTTPClient, -) -from opentelemetry.exporter.otlp.json.common._internal._log_encoder import ( - encode_logs, -) -from opentelemetry.exporter.otlp.json.http._internal import ( - _build_transport, - _resolve_compression, - _resolve_endpoint, - _resolve_headers, - _resolve_timeout, -) -from opentelemetry.sdk._shared_internal import DuplicateFilter -from opentelemetry.sdk.environment_variables import ( - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, - OTEL_EXPORTER_OTLP_LOGS_HEADERS, - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, -) -from opentelemetry.sdk.logs import ReadableLogRecord -from opentelemetry.sdk.logs.export import ( - LogRecordExporter, - LogRecordExportResult, -) - -_DEFAULT_LOGS_EXPORT_PATH = "v1/logs" - -_logger = logging.getLogger(__name__) -_logger.addFilter(DuplicateFilter()) +import warnings +from opentelemetry.exporter.otlp.json.http.log_exporter import ( + OTLPLogExporter, +) -class OTLPLogExporter(LogRecordExporter): - @overload - def __init__( - self, - endpoint: str | None = None, - certificate_file: str | None = None, - client_key_file: str | None = None, - client_certificate_file: str | None = None, - headers: Mapping[str, str] | None = None, - timeout: float | None = None, - compression: Compression | None = None, - ) -> None: ... - - @overload - def __init__( - self, - endpoint: str | None = None, - certificate_file: None = None, - client_key_file: None = None, - client_certificate_file: None = None, - headers: Mapping[str, str] | None = None, - timeout: float | None = None, - compression: Compression | None = None, - *, - _transport: BaseHTTPTransport, - ) -> None: ... - - def __init__( - self, - endpoint: str | None = None, - certificate_file: str | None = None, - client_key_file: str | None = None, - client_certificate_file: str | None = None, - headers: Mapping[str, str] | None = None, - timeout: float | None = None, - compression: Compression | None = None, - *, - _transport: BaseHTTPTransport | None = None, - ) -> None: - """OTLP HTTP JSON log exporter. - - Args: - endpoint: Full URL of the OTLP/HTTP signal endpoint, including the signal - path. Example: ``http://collector:4318/v1/logs``. For a base URL without a - signal path, set the ``OTEL_EXPORTER_OTLP_ENDPOINT`` environment variable. - """ - transport = _transport or _build_transport( - certificate_file, - client_key_file, - client_certificate_file, - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, - ) - self._client = _OTLPHTTPClient( - transport=transport, - endpoint=endpoint or _resolve_endpoint(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, _DEFAULT_LOGS_EXPORT_PATH), - kind="logs", - timeout=timeout if timeout is not None else _resolve_timeout(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT), - compression=compression - if compression is not None - else _resolve_compression(OTEL_EXPORTER_OTLP_LOGS_COMPRESSION), - headers=_resolve_headers(headers, OTEL_EXPORTER_OTLP_LOGS_HEADERS), - logger=_logger, - ) - self._shutdown = False - - def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: - if self._shutdown: - _logger.warning("Exporter already shutdown, ignoring batch") - return LogRecordExportResult.FAILURE - try: - body = encode_logs(batch).to_json().encode() - # pylint: disable-next=broad-exception-caught - except Exception as error: - _logger.error("Failed to encode logs: %s", error) - return LogRecordExportResult.FAILURE - export_result = self._client.export(body) - return LogRecordExportResult.SUCCESS if export_result.success else LogRecordExportResult.FAILURE - - def shutdown(self) -> None: - if self._shutdown: - _logger.warning("Exporter already shutdown, ignoring call") - return - self._shutdown = True - self._client.shutdown() +warnings.warn( + "The opentelemetry.exporter.otlp.json.http._log_exporter module is deprecated since version 1.46.0 " + "and will be removed in a future release. " + "Use opentelemetry.exporter.otlp.json.http.log_exporter instead.", + DeprecationWarning, + stacklevel=2, +) - # pylint: disable-next=no-self-use - def force_flush(self, timeout_millis: int = 10_000) -> bool: - return True +__all__ = [ + "OTLPLogExporter", +] diff --git a/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/log_exporter.py b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/log_exporter.py new file mode 100644 index 00000000000..b5419c5e8ef --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/src/opentelemetry/exporter/otlp/json/http/log_exporter.py @@ -0,0 +1,134 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import logging +from collections.abc import Mapping, Sequence +from typing import overload + +from opentelemetry.exporter.http.transport._base import BaseHTTPTransport +from opentelemetry.exporter.otlp.common.http import ( + Compression, + _OTLPHTTPClient, +) +from opentelemetry.exporter.otlp.json.common._internal.log_encoder import ( + encode_logs, +) +from opentelemetry.exporter.otlp.json.http._internal import ( + _build_transport, + _resolve_compression, + _resolve_endpoint, + _resolve_headers, + _resolve_timeout, +) +from opentelemetry.sdk._shared_internal import DuplicateFilter +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, +) +from opentelemetry.sdk.logs import ReadableLogRecord +from opentelemetry.sdk.logs.export import ( + LogRecordExporter, + LogRecordExportResult, +) + +_DEFAULT_LOGS_EXPORT_PATH = "v1/logs" + +_logger = logging.getLogger(__name__) +_logger.addFilter(DuplicateFilter()) + + +class OTLPLogExporter(LogRecordExporter): + @overload + def __init__( + self, + endpoint: str | None = None, + certificate_file: str | None = None, + client_key_file: str | None = None, + client_certificate_file: str | None = None, + headers: Mapping[str, str] | None = None, + timeout: float | None = None, + compression: Compression | None = None, + ) -> None: ... + + @overload + def __init__( + self, + endpoint: str | None = None, + certificate_file: None = None, + client_key_file: None = None, + client_certificate_file: None = None, + headers: Mapping[str, str] | None = None, + timeout: float | None = None, + compression: Compression | None = None, + *, + _transport: BaseHTTPTransport, + ) -> None: ... + + def __init__( + self, + endpoint: str | None = None, + certificate_file: str | None = None, + client_key_file: str | None = None, + client_certificate_file: str | None = None, + headers: Mapping[str, str] | None = None, + timeout: float | None = None, + compression: Compression | None = None, + *, + _transport: BaseHTTPTransport | None = None, + ) -> None: + """OTLP HTTP JSON log exporter. + + Args: + endpoint: Full URL of the OTLP/HTTP signal endpoint, including the signal + path. Example: ``http://collector:4318/v1/logs``. For a base URL without a + signal path, set the ``OTEL_EXPORTER_OTLP_ENDPOINT`` environment variable. + """ + transport = _transport or _build_transport( + certificate_file, + client_key_file, + client_certificate_file, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + ) + self._client = _OTLPHTTPClient( + transport=transport, + endpoint=endpoint or _resolve_endpoint(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, _DEFAULT_LOGS_EXPORT_PATH), + kind="logs", + timeout=timeout if timeout is not None else _resolve_timeout(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT), + compression=compression + if compression is not None + else _resolve_compression(OTEL_EXPORTER_OTLP_LOGS_COMPRESSION), + headers=_resolve_headers(headers, OTEL_EXPORTER_OTLP_LOGS_HEADERS), + logger=_logger, + ) + self._shutdown = False + + def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring batch") + return LogRecordExportResult.FAILURE + try: + body = encode_logs(batch).to_json().encode() + # pylint: disable-next=broad-exception-caught + except Exception as error: + _logger.error("Failed to encode logs: %s", error) + return LogRecordExportResult.FAILURE + export_result = self._client.export(body) + return LogRecordExportResult.SUCCESS if export_result.success else LogRecordExportResult.FAILURE + + def shutdown(self) -> None: + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring call") + return + self._shutdown = True + self._client.shutdown() + + # pylint: disable-next=no-self-use + def force_flush(self, timeout_millis: int = 10_000) -> bool: + return True diff --git a/exporter/opentelemetry-exporter-otlp-json-http/tests/test_deprecated_log_module.py b/exporter/opentelemetry-exporter-otlp-json-http/tests/test_deprecated_log_module.py new file mode 100644 index 00000000000..87cd57dd569 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-json-http/tests/test_deprecated_log_module.py @@ -0,0 +1,60 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import importlib +import sys +import unittest +from importlib.metadata import EntryPoint + +import opentelemetry.exporter.otlp.json.http.log_exporter + +_DEPRECATED_MODULE = "opentelemetry.exporter.otlp.json.http._log_exporter" + + +def _unload_deprecated_module() -> None: + sys.modules.pop(_DEPRECATED_MODULE, None) + sys.modules["opentelemetry.exporter.otlp.json.http"].__dict__.pop("_log_exporter", None) + + +class TestDeprecatedLogModule(unittest.TestCase): + def setUp(self) -> None: + _unload_deprecated_module() + + def test_import_emits_deprecation_warning(self) -> None: + with self.assertWarns(DeprecationWarning) as context: + importlib.import_module(_DEPRECATED_MODULE) + message = str(context.warning) + self.assertIn("opentelemetry.exporter.otlp.json.http.log_exporter", message) + self.assertIn("will be removed in a future release", message) + + def test_deprecation_warning_points_at_importer(self) -> None: + # pylint: disable=import-outside-toplevel,unused-import + # A literal import statement is required here since importlib.import_module + # would attribute the warning to importlib itself. + with self.assertWarns(DeprecationWarning) as context: + from opentelemetry.exporter.otlp.json.http._log_exporter import OTLPLogExporter # noqa: F401, PLC0415 + self.assertEqual(context.filename, __file__) + + def test_reexports_are_identical(self) -> None: + with self.assertWarns(DeprecationWarning): + deprecated = importlib.import_module(_DEPRECATED_MODULE) + + self.assertEqual(deprecated.__all__, ["OTLPLogExporter"]) + for name in deprecated.__all__: + with self.subTest(name=name): + self.assertIs( + getattr(deprecated, name), getattr(opentelemetry.exporter.otlp.json.http.log_exporter, name) + ) + + def test_old_entry_point_target_resolves(self) -> None: + # Entry points and configuration may still reference the old module path. + entry_point = EntryPoint( + name="log_exporter", + value="opentelemetry.exporter.otlp.json.http._log_exporter:OTLPLogExporter", + group="test", + ) + with self.assertWarns(DeprecationWarning): + target = entry_point.load() + self.assertIs(target, opentelemetry.exporter.otlp.json.http.log_exporter.OTLPLogExporter) diff --git a/exporter/opentelemetry-exporter-otlp-json-http/tests/test_log_exporter.py b/exporter/opentelemetry-exporter-otlp-json-http/tests/test_log_exporter.py index 5bf4f97583a..5e881ce21c9 100644 --- a/exporter/opentelemetry-exporter-otlp-json-http/tests/test_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-json-http/tests/test_log_exporter.py @@ -21,9 +21,10 @@ Urllib3HTTPTransport, ) from opentelemetry.exporter.otlp.common.http import Compression -from opentelemetry.exporter.otlp.json.common._log_encoder import encode_logs +from opentelemetry.exporter.otlp.json import http as otlp_json_http +from opentelemetry.exporter.otlp.json.common.log_encoder import encode_logs from opentelemetry.exporter.otlp.json.http._internal import _build_transport -from opentelemetry.exporter.otlp.json.http._log_exporter import ( +from opentelemetry.exporter.otlp.json.http.log_exporter import ( OTLPLogExporter, ) from opentelemetry.logs import LogRecord, SeverityNumber @@ -55,7 +56,7 @@ from . import _mock_clock _TEST_ENDPOINT = "http://localhost:4318/v1/logs" -_LOGGER_NAME = "opentelemetry.exporter.otlp.json.http._log_exporter" +_LOGGER_NAME = "opentelemetry.exporter.otlp.json.http.log_exporter" class TestOTLPLogExporter(unittest.TestCase): @@ -82,6 +83,10 @@ def _mocked_shutdown_event() -> Mock: shutdown_event.is_set.return_value = False return shutdown_event + def test_exported_at_package_level(self): + self.assertIs(otlp_json_http.OTLPLogExporter, OTLPLogExporter) + self.assertIn("OTLPLogExporter", otlp_json_http.__all__) + @mocketize def test_export_single_log(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) @@ -252,7 +257,7 @@ def test_custom_transport(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) custom_transport = Urllib3HTTPTransport() - with patch("opentelemetry.exporter.otlp.json.http._log_exporter._build_transport") as mock_build_transport: + with patch("opentelemetry.exporter.otlp.json.http.log_exporter._build_transport") as mock_build_transport: exporter = OTLPLogExporter(endpoint=_TEST_ENDPOINT, _transport=custom_transport) mock_build_transport.assert_not_called() @@ -298,7 +303,7 @@ def test_certificate_args(self): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) with patch( - "opentelemetry.exporter.otlp.json.http._log_exporter._build_transport", + "opentelemetry.exporter.otlp.json.http.log_exporter._build_transport", wraps=_build_transport, ) as mock_build_transport: exporter = OTLPLogExporter( @@ -503,7 +508,7 @@ def test_export_encoding_failure(self): with ( patch( - "opentelemetry.exporter.otlp.json.http._log_exporter.encode_logs", + "opentelemetry.exporter.otlp.json.http.log_exporter.encode_logs", side_effect=ValueError("boom"), ), self.assertLogs(_LOGGER_NAME, level="ERROR"), diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/log_encoder/__init__.py similarity index 100% rename from exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/_log_encoder/__init__.py rename to exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_internal/log_encoder/__init__.py diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_log_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_log_encoder.py index e55fa059673..3c355623dcb 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_log_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/_log_encoder.py @@ -1,9 +1,27 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +""" +Deprecated alias of :mod:`opentelemetry.exporter.otlp.proto.common.log_encoder`. +.. deprecated:: 1.46.0 + This module will be removed in a future release. Use + :mod:`opentelemetry.exporter.otlp.proto.common.log_encoder` instead. +""" -from opentelemetry.exporter.otlp.proto.common._internal._log_encoder import ( +import warnings + +from opentelemetry.exporter.otlp.proto.common.log_encoder import ( encode_logs, ) -__all__ = ["encode_logs"] +warnings.warn( + "The opentelemetry.exporter.otlp.proto.common._log_encoder module is deprecated since version 1.46.0 " + "and will be removed in a future release. " + "Use opentelemetry.exporter.otlp.proto.common.log_encoder instead.", + DeprecationWarning, + stacklevel=2, +) + +__all__ = [ + "encode_logs", +] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/log_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/log_encoder.py new file mode 100644 index 00000000000..7659eda5710 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/src/opentelemetry/exporter/otlp/proto/common/log_encoder.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + + +from opentelemetry.exporter.otlp.proto.common._internal.log_encoder import ( + encode_logs, +) + +__all__ = ["encode_logs"] diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_bytes_resource.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_bytes_resource.py index 01c219b7ae1..d8c14a0d0f6 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_bytes_resource.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_bytes_resource.py @@ -7,7 +7,7 @@ import unittest -from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs +from opentelemetry.exporter.otlp.proto.common.log_encoder import encode_logs from opentelemetry.exporter.otlp.proto.common.metrics_encoder import ( encode_metrics, ) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_deprecated_log_module.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_deprecated_log_module.py new file mode 100644 index 00000000000..f5d571d3e39 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_deprecated_log_module.py @@ -0,0 +1,58 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import importlib +import sys +import unittest +from importlib.metadata import EntryPoint + +import opentelemetry.exporter.otlp.proto.common.log_encoder + +_DEPRECATED_MODULE = "opentelemetry.exporter.otlp.proto.common._log_encoder" + + +def _unload_deprecated_module() -> None: + sys.modules.pop(_DEPRECATED_MODULE, None) + sys.modules["opentelemetry.exporter.otlp.proto.common"].__dict__.pop("_log_encoder", None) + + +class TestDeprecatedLogModule(unittest.TestCase): + def setUp(self) -> None: + _unload_deprecated_module() + + def test_import_emits_deprecation_warning(self) -> None: + with self.assertWarns(DeprecationWarning) as context: + importlib.import_module(_DEPRECATED_MODULE) + message = str(context.warning) + self.assertIn("opentelemetry.exporter.otlp.proto.common.log_encoder", message) + self.assertIn("will be removed in a future release", message) + + def test_deprecation_warning_points_at_importer(self) -> None: + # pylint: disable=import-outside-toplevel,unused-import + # A literal import statement is required here since importlib.import_module + # would attribute the warning to importlib itself. + with self.assertWarns(DeprecationWarning) as context: + from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs # noqa: F401, PLC0415 + self.assertEqual(context.filename, __file__) + + def test_reexports_are_identical(self) -> None: + with self.assertWarns(DeprecationWarning): + deprecated = importlib.import_module(_DEPRECATED_MODULE) + + self.assertEqual(deprecated.__all__, ["encode_logs"]) + for name in deprecated.__all__: + with self.subTest(name=name): + self.assertIs( + getattr(deprecated, name), getattr(opentelemetry.exporter.otlp.proto.common.log_encoder, name) + ) + + def test_old_entry_point_target_resolves(self) -> None: + # Entry points and configuration may still reference the old module path. + entry_point = EntryPoint( + name="log_encoder", value="opentelemetry.exporter.otlp.proto.common._log_encoder:encode_logs", group="test" + ) + with self.assertWarns(DeprecationWarning): + target = entry_point.load() + self.assertIs(target, opentelemetry.exporter.otlp.proto.common.log_encoder.encode_logs) diff --git a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py index a6cc81e86e9..d9bd0b4aca8 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py +++ b/exporter/opentelemetry-exporter-otlp-proto-common/tests/test_log_encoder.py @@ -9,7 +9,7 @@ _encode_trace_id, _encode_value, ) -from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs +from opentelemetry.exporter.otlp.proto.common.log_encoder import encode_logs from opentelemetry.logs import LogRecord, SeverityNumber from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( ExportLogsServiceRequest, diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml index 2d88a04d3bd..9e46df3ada7 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/pyproject.toml @@ -39,7 +39,7 @@ dependencies = [ ] [project.entry-points.opentelemetry_logs_exporter] -otlp_proto_grpc = "opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter" +otlp_proto_grpc = "opentelemetry.exporter.otlp.proto.grpc.log_exporter:OTLPLogExporter" [project.entry-points.opentelemetry_metrics_exporter] otlp_proto_grpc = "opentelemetry.exporter.otlp.proto.grpc.metric_exporter:OTLPMetricExporter" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py index 24af969db81..3befac6875e 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/_log_exporter/__init__.py @@ -1,124 +1,27 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 +""" +Deprecated alias of :mod:`opentelemetry.exporter.otlp.proto.grpc.log_exporter`. -from collections.abc import Iterable, Sequence -from collections.abc import Sequence as TypingSequence -from os import environ -from typing import Literal +.. deprecated:: 1.46.0 + This module will be removed in a future release. Use + :mod:`opentelemetry.exporter.otlp.proto.grpc.log_exporter` instead. +""" -from grpc import ChannelCredentials, Compression, StatusCode -from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs -from opentelemetry.exporter.otlp.proto.grpc.exporter import ( - OTLPExporterMixin, - _get_credentials, - environ_to_compression, -) -from opentelemetry.metrics import MeterProvider -from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( - ExportLogsServiceRequest, -) -from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( - LogsServiceStub, -) -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, - OTEL_EXPORTER_OTLP_LOGS_HEADERS, - OTEL_EXPORTER_OTLP_LOGS_INSECURE, - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, -) -from opentelemetry.sdk.logs import ReadableLogRecord -from opentelemetry.sdk.logs.export import ( - LogRecordExporter, - LogRecordExportResult, -) -from opentelemetry.semconv._incubating.attributes.otel_attributes import ( - OtelComponentTypeValues, -) - - -class OTLPLogExporter( - LogRecordExporter, - OTLPExporterMixin[ - Sequence[ReadableLogRecord], - ExportLogsServiceRequest, - LogRecordExportResult, - LogsServiceStub, - ], -): - def __init__( - self, - endpoint: str | None = None, - insecure: bool | None = None, - credentials: ChannelCredentials | None = None, - headers: TypingSequence[tuple[str, str]] | dict[str, str] | str | None = None, - timeout: float | None = None, - compression: Compression | None = None, - channel_options: tuple[tuple[str, str]] | None = None, - retryable_error_codes: Iterable[StatusCode] | None = None, - *, - meter_provider: MeterProvider | None = None, - ): - insecure_logs = environ.get(OTEL_EXPORTER_OTLP_LOGS_INSECURE) - if insecure is None and insecure_logs is not None: - insecure = insecure_logs.lower() == "true" - - if not insecure and environ.get(OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE) is not None: - credentials = _get_credentials( - credentials, - _OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, - ) +import warnings - environ_timeout = environ.get(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT) - environ_timeout = float(environ_timeout) if environ_timeout is not None else None - - compression = ( - environ_to_compression(OTEL_EXPORTER_OTLP_LOGS_COMPRESSION) if compression is None else compression - ) - - OTLPExporterMixin.__init__( - self, - endpoint=endpoint or environ.get(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT), - insecure=insecure, - credentials=credentials, - headers=headers or environ.get(OTEL_EXPORTER_OTLP_LOGS_HEADERS), - timeout=timeout or environ_timeout, - compression=compression, - stub=LogsServiceStub, - result=LogRecordExportResult, - channel_options=channel_options, - retryable_error_codes=retryable_error_codes, - component_type=OtelComponentTypeValues.OTLP_GRPC_LOG_EXPORTER, - signal="logs", - meter_provider=meter_provider, - ) - - def _translate_data(self, data: Sequence[ReadableLogRecord]) -> ExportLogsServiceRequest: - return encode_logs(data) - - def _count_data(self, data: Sequence[ReadableLogRecord]): - return len(data) - - def export( # type: ignore [reportIncompatibleMethodOverride] - self, - batch: Sequence[ReadableLogRecord], - ) -> Literal[LogRecordExportResult.SUCCESS, LogRecordExportResult.FAILURE]: - return OTLPExporterMixin._export(self, batch) - - def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: - OTLPExporterMixin.shutdown(self, timeout_millis=timeout_millis) +from opentelemetry.exporter.otlp.proto.grpc.log_exporter import ( + OTLPLogExporter, +) - def force_flush(self, timeout_millis: int = 10_000) -> bool: - """Nothing is buffered in this exporter, so this method does nothing.""" - return True +warnings.warn( + "The opentelemetry.exporter.otlp.proto.grpc._log_exporter module is deprecated since version 1.46.0 " + "and will be removed in a future release. " + "Use opentelemetry.exporter.otlp.proto.grpc.log_exporter instead.", + DeprecationWarning, + stacklevel=2, +) - @property - def _exporting(self) -> str: - return "logs" +__all__ = [ + "OTLPLogExporter", +] diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py new file mode 100644 index 00000000000..b1c9d999276 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/src/opentelemetry/exporter/otlp/proto/grpc/log_exporter/__init__.py @@ -0,0 +1,124 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from collections.abc import Iterable, Sequence +from collections.abc import Sequence as TypingSequence +from os import environ +from typing import Literal + +from grpc import ChannelCredentials, Compression, StatusCode +from opentelemetry.exporter.otlp.proto.common.log_encoder import encode_logs +from opentelemetry.exporter.otlp.proto.grpc.exporter import ( + OTLPExporterMixin, + _get_credentials, + environ_to_compression, +) +from opentelemetry.metrics import MeterProvider +from opentelemetry.proto.collector.logs.v1.logs_service_pb2 import ( + ExportLogsServiceRequest, +) +from opentelemetry.proto.collector.logs.v1.logs_service_pb2_grpc import ( + LogsServiceStub, +) +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_INSECURE, + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, +) +from opentelemetry.sdk.logs import ReadableLogRecord +from opentelemetry.sdk.logs.export import ( + LogRecordExporter, + LogRecordExportResult, +) +from opentelemetry.semconv._incubating.attributes.otel_attributes import ( + OtelComponentTypeValues, +) + + +class OTLPLogExporter( + LogRecordExporter, + OTLPExporterMixin[ + Sequence[ReadableLogRecord], + ExportLogsServiceRequest, + LogRecordExportResult, + LogsServiceStub, + ], +): + def __init__( + self, + endpoint: str | None = None, + insecure: bool | None = None, + credentials: ChannelCredentials | None = None, + headers: TypingSequence[tuple[str, str]] | dict[str, str] | str | None = None, + timeout: float | None = None, + compression: Compression | None = None, + channel_options: tuple[tuple[str, str]] | None = None, + retryable_error_codes: Iterable[StatusCode] | None = None, + *, + meter_provider: MeterProvider | None = None, + ): + insecure_logs = environ.get(OTEL_EXPORTER_OTLP_LOGS_INSECURE) + if insecure is None and insecure_logs is not None: + insecure = insecure_logs.lower() == "true" + + if not insecure and environ.get(OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE) is not None: + credentials = _get_credentials( + credentials, + _OTEL_PYTHON_EXPORTER_OTLP_GRPC_LOGS_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + ) + + environ_timeout = environ.get(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT) + environ_timeout = float(environ_timeout) if environ_timeout is not None else None + + compression = ( + environ_to_compression(OTEL_EXPORTER_OTLP_LOGS_COMPRESSION) if compression is None else compression + ) + + OTLPExporterMixin.__init__( + self, + endpoint=endpoint or environ.get(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT), + insecure=insecure, + credentials=credentials, + headers=headers or environ.get(OTEL_EXPORTER_OTLP_LOGS_HEADERS), + timeout=timeout or environ_timeout, + compression=compression, + stub=LogsServiceStub, + result=LogRecordExportResult, + channel_options=channel_options, + retryable_error_codes=retryable_error_codes, + component_type=OtelComponentTypeValues.OTLP_GRPC_LOG_EXPORTER, + signal="logs", + meter_provider=meter_provider, + ) + + def _translate_data(self, data: Sequence[ReadableLogRecord]) -> ExportLogsServiceRequest: + return encode_logs(data) + + def _count_data(self, data: Sequence[ReadableLogRecord]): + return len(data) + + def export( # type: ignore [reportIncompatibleMethodOverride] + self, + batch: Sequence[ReadableLogRecord], + ) -> Literal[LogRecordExportResult.SUCCESS, LogRecordExportResult.FAILURE]: + return OTLPExporterMixin._export(self, batch) + + def shutdown(self, timeout_millis: float = 30_000, **kwargs) -> None: + OTLPExporterMixin.shutdown(self, timeout_millis=timeout_millis) + + def force_flush(self, timeout_millis: int = 10_000) -> bool: + """Nothing is buffered in this exporter, so this method does nothing.""" + return True + + @property + def _exporting(self) -> str: + return "logs" diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_deprecated_log_module.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_deprecated_log_module.py new file mode 100644 index 00000000000..202f8c4908d --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_deprecated_log_module.py @@ -0,0 +1,60 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import importlib +import sys +import unittest +from importlib.metadata import EntryPoint + +import opentelemetry.exporter.otlp.proto.grpc.log_exporter + +_DEPRECATED_MODULE = "opentelemetry.exporter.otlp.proto.grpc._log_exporter" + + +def _unload_deprecated_module() -> None: + sys.modules.pop(_DEPRECATED_MODULE, None) + sys.modules["opentelemetry.exporter.otlp.proto.grpc"].__dict__.pop("_log_exporter", None) + + +class TestDeprecatedLogModule(unittest.TestCase): + def setUp(self) -> None: + _unload_deprecated_module() + + def test_import_emits_deprecation_warning(self) -> None: + with self.assertWarns(DeprecationWarning) as context: + importlib.import_module(_DEPRECATED_MODULE) + message = str(context.warning) + self.assertIn("opentelemetry.exporter.otlp.proto.grpc.log_exporter", message) + self.assertIn("will be removed in a future release", message) + + def test_deprecation_warning_points_at_importer(self) -> None: + # pylint: disable=import-outside-toplevel,unused-import + # A literal import statement is required here since importlib.import_module + # would attribute the warning to importlib itself. + with self.assertWarns(DeprecationWarning) as context: + from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter # noqa: F401, PLC0415 + self.assertEqual(context.filename, __file__) + + def test_reexports_are_identical(self) -> None: + with self.assertWarns(DeprecationWarning): + deprecated = importlib.import_module(_DEPRECATED_MODULE) + + self.assertEqual(deprecated.__all__, ["OTLPLogExporter"]) + for name in deprecated.__all__: + with self.subTest(name=name): + self.assertIs( + getattr(deprecated, name), getattr(opentelemetry.exporter.otlp.proto.grpc.log_exporter, name) + ) + + def test_old_entry_point_target_resolves(self) -> None: + # Entry points and configuration may still reference the old module path. + entry_point = EntryPoint( + name="log_exporter", + value="opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter", + group="test", + ) + with self.assertWarns(DeprecationWarning): + target = entry_point.load() + self.assertIs(target, opentelemetry.exporter.otlp.proto.grpc.log_exporter.OTLPLogExporter) diff --git a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py index 2ea76cf13ea..8da8badefcf 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-grpc/tests/logs/test_otlp_logs_exporter.py @@ -12,7 +12,7 @@ from grpc import ChannelCredentials, Compression from opentelemetry.exporter.otlp.proto.common._internal import _encode_value -from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( +from opentelemetry.exporter.otlp.proto.grpc.log_exporter import ( OTLPLogExporter, ) from opentelemetry.logs import LogRecord, SeverityNumber diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml index 5e37ee2fac8..7bf0cfa9736 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp-proto-http/pyproject.toml @@ -44,7 +44,7 @@ otlp_proto_http = "opentelemetry.exporter.otlp.proto.http.trace_exporter:OTLPSpa otlp_proto_http = "opentelemetry.exporter.otlp.proto.http.metric_exporter:OTLPMetricExporter" [project.entry-points.opentelemetry_logs_exporter] -otlp_proto_http = "opentelemetry.exporter.otlp.proto.http._log_exporter:OTLPLogExporter" +otlp_proto_http = "opentelemetry.exporter.otlp.proto.http.log_exporter:OTLPLogExporter" [project.urls] Homepage = "https://github.com/open-telemetry/opentelemetry-python/tree/main/exporter/opentelemetry-exporter-otlp-proto-http" diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py index 8def20cedc5..82e1dc08921 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/_log_exporter/__init__.py @@ -1,223 +1,35 @@ # Copyright The OpenTelemetry Authors # SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -import logging -import os -from collections.abc import Mapping, Sequence -from typing import TYPE_CHECKING, overload - -from opentelemetry.exporter.otlp.common import http as _http -from opentelemetry.exporter.otlp.common._exporter_metrics import ( - create_exporter_metrics, -) -from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs -from opentelemetry.exporter.otlp.proto.http import Compression -from opentelemetry.exporter.otlp.proto.http._common import ( - _DEFAULT_MAX_REQUEST_SIZE, - RequestPayloadTooLargeError, - _build_transport, - _is_request_too_large, - _load_session_from_envvar, - _normalize_compression, - _resolve_compression, - _resolve_endpoint, - _resolve_headers, - _resolve_timeout, -) -from opentelemetry.metrics import MeterProvider -from opentelemetry.sdk._shared_internal import DuplicateFilter -from opentelemetry.sdk.environment_variables import ( - _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, - OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, - OTEL_EXPORTER_OTLP_LOGS_HEADERS, - OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, - OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, -) -from opentelemetry.sdk.logs import ReadableLogRecord -from opentelemetry.sdk.logs.export import ( - LogRecordExporter, - LogRecordExportResult, +""" +Deprecated alias of :mod:`opentelemetry.exporter.otlp.proto.http.log_exporter`. + +.. deprecated:: 1.46.0 + This module will be removed in a future release. Use + :mod:`opentelemetry.exporter.otlp.proto.http.log_exporter` instead. +""" + +import warnings + +from opentelemetry.exporter.otlp.proto.http.log_exporter import ( + DEFAULT_COMPRESSION, + DEFAULT_ENDPOINT, + DEFAULT_LOGS_EXPORT_PATH, + DEFAULT_TIMEOUT, + OTLPLogExporter, ) -from opentelemetry.semconv._incubating.attributes.otel_attributes import ( - OtelComponentTypeValues, -) -from opentelemetry.semconv.attributes.http_attributes import ( - HTTP_RESPONSE_STATUS_CODE, -) - -if TYPE_CHECKING: - import requests - - from opentelemetry.exporter.http.transport._base import BaseHTTPTransport - -_logger = logging.getLogger(__name__) -# This prevents logs generated when a log fails to be written to generate another log which fails to be written etc. etc. -_logger.addFilter(DuplicateFilter()) - -DEFAULT_COMPRESSION = Compression.NoCompression -DEFAULT_ENDPOINT = "http://localhost:4318/" -DEFAULT_LOGS_EXPORT_PATH = "v1/logs" -DEFAULT_TIMEOUT = 10 # in seconds - - -class OTLPLogExporter(LogRecordExporter): - @overload - def __init__( - self, - endpoint: str | None = None, - certificate_file: str | None = None, - client_key_file: str | None = None, - client_certificate_file: str | None = None, - headers: Mapping[str, str] | None = None, - timeout: float | None = None, - compression: Compression | _http.Compression | None = None, - session: requests.Session | None = None, - *, - max_request_size: int | None = None, - meter_provider: MeterProvider | None = None, - ) -> None: ... - - @overload - def __init__( - self, - endpoint: str | None = None, - certificate_file: None = None, - client_key_file: None = None, - client_certificate_file: None = None, - headers: Mapping[str, str] | None = None, - timeout: float | None = None, - compression: Compression | _http.Compression | None = None, - session: requests.Session | None = None, - *, - max_request_size: int | None = None, - meter_provider: MeterProvider | None = None, - _transport: BaseHTTPTransport, - ) -> None: ... - - def __init__( - self, - endpoint: str | None = None, - certificate_file: str | None = None, - client_key_file: str | None = None, - client_certificate_file: str | None = None, - headers: Mapping[str, str] | None = None, - timeout: float | None = None, - compression: Compression | _http.Compression | None = None, - session: requests.Session | None = None, - *, - max_request_size: int | None = None, - meter_provider: MeterProvider | None = None, - _transport: BaseHTTPTransport | None = None, - ) -> None: - """OTLP HTTP log exporter. - - Args: - endpoint: Full URL of the OTLP/HTTP signal endpoint, including the signal - path. Example: ``http://collector:4318/v1/logs``. For a base URL without a - signal path, set the ``OTEL_EXPORTER_OTLP_ENDPOINT`` environment variable. - certificate_file: Path to the CA certificate file for TLS. - client_key_file: Path to the client key file for mTLS. - client_certificate_file: Path to the client certificate file for mTLS. - headers: Headers to send with each export request. - timeout: Timeout in seconds for each export request. - compression: Compression to use; one of none, gzip, deflate. - session: Requests session to use at export. - max_request_size: Maximum size in bytes of a serialized request, - measured before compression. A request exceeding this size is - dropped before being sent. Defaults to 64 MiB; a value of 0 (or - any non-positive value) disables the limit. Batch processors - group log records by count rather than serialized size, so a - batch whose serialized request exceeds this limit is dropped as - a whole and recorded as a failed export; reduce the processor's - ``max_export_batch_size`` (or raise/disable this limit) if - batches may approach it. - meter_provider: MeterProvider used for the exporter's own metrics. - """ - self._endpoint = endpoint or _resolve_endpoint(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, DEFAULT_LOGS_EXPORT_PATH) - self._compression = _normalize_compression(compression) or _resolve_compression( - OTEL_EXPORTER_OTLP_LOGS_COMPRESSION - ) - transport = _transport or _build_transport( - certificate_file, - client_key_file, - client_certificate_file, - OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, - OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, - session=session or _load_session_from_envvar(_OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER), - ) - self._max_request_size = _DEFAULT_MAX_REQUEST_SIZE if max_request_size is None else max_request_size - self._client = _http._OTLPHTTPClient( - transport=transport, - endpoint=self._endpoint, - kind="logs", - timeout=timeout if timeout is not None else _resolve_timeout(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT), - compression=self._compression, - headers=_resolve_headers(headers, OTEL_EXPORTER_OTLP_LOGS_HEADERS), - logger=_logger, - ) - self._shutdown = False - - self._metrics = create_exporter_metrics( - OtelComponentTypeValues.OTLP_HTTP_LOG_EXPORTER, - "logs", - self._endpoint, - meter_provider, - os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", - ) - - def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: - if self._shutdown: - _logger.warning("Exporter already shutdown, ignoring batch") - return LogRecordExportResult.FAILURE - - with self._metrics.export_operation(len(batch)) as result: - try: - serialized_data = encode_logs(batch).SerializeToString() - # pylint: disable-next=broad-exception-caught - except Exception as error: - _logger.error("Failed to encode logs batch: %s", error) - result.error = error - return LogRecordExportResult.FAILURE - - if _is_request_too_large(serialized_data, self._max_request_size): - _logger.warning( - "Dropping logs batch: serialized size %d bytes exceeds max_request_size %d bytes.", - len(serialized_data), - self._max_request_size, - ) - result.error = RequestPayloadTooLargeError( - f"Serialized logs request size {len(serialized_data)} " - f"bytes exceeds max_request_size " - f"{self._max_request_size} bytes." - ) - return LogRecordExportResult.FAILURE - - export_result = self._client.export(serialized_data) - if not export_result.success: - result.error = export_result.error - result.error_attrs = ( - {HTTP_RESPONSE_STATUS_CODE: export_result.status_code} - if export_result.status_code is not None - else None - ) - return LogRecordExportResult.FAILURE - return LogRecordExportResult.SUCCESS - - def force_flush(self, timeout_millis: int = 10_000) -> bool: - """Nothing is buffered in this exporter, so this method does nothing.""" - return True +warnings.warn( + "The opentelemetry.exporter.otlp.proto.http._log_exporter module is deprecated since version 1.46.0 " + "and will be removed in a future release. " + "Use opentelemetry.exporter.otlp.proto.http.log_exporter instead.", + DeprecationWarning, + stacklevel=2, +) - def shutdown(self): - if self._shutdown: - _logger.warning("Exporter already shutdown, ignoring call") - return - self._shutdown = True - self._client.shutdown() +__all__ = [ + "DEFAULT_COMPRESSION", + "DEFAULT_ENDPOINT", + "DEFAULT_LOGS_EXPORT_PATH", + "DEFAULT_TIMEOUT", + "OTLPLogExporter", +] diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/log_exporter/__init__.py b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/log_exporter/__init__.py new file mode 100644 index 00000000000..56e6161fd77 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/src/opentelemetry/exporter/otlp/proto/http/log_exporter/__init__.py @@ -0,0 +1,223 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import logging +import os +from collections.abc import Mapping, Sequence +from typing import TYPE_CHECKING, overload + +from opentelemetry.exporter.otlp.common import http as _http +from opentelemetry.exporter.otlp.common._exporter_metrics import ( + create_exporter_metrics, +) +from opentelemetry.exporter.otlp.proto.common.log_encoder import encode_logs +from opentelemetry.exporter.otlp.proto.http import Compression +from opentelemetry.exporter.otlp.proto.http._common import ( + _DEFAULT_MAX_REQUEST_SIZE, + RequestPayloadTooLargeError, + _build_transport, + _is_request_too_large, + _load_session_from_envvar, + _normalize_compression, + _resolve_compression, + _resolve_endpoint, + _resolve_headers, + _resolve_timeout, +) +from opentelemetry.metrics import MeterProvider +from opentelemetry.sdk._shared_internal import DuplicateFilter +from opentelemetry.sdk.environment_variables import ( + _OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION, + OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, + OTEL_EXPORTER_OTLP_LOGS_HEADERS, + OTEL_EXPORTER_OTLP_LOGS_TIMEOUT, + OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, +) +from opentelemetry.sdk.logs import ReadableLogRecord +from opentelemetry.sdk.logs.export import ( + LogRecordExporter, + LogRecordExportResult, +) +from opentelemetry.semconv._incubating.attributes.otel_attributes import ( + OtelComponentTypeValues, +) +from opentelemetry.semconv.attributes.http_attributes import ( + HTTP_RESPONSE_STATUS_CODE, +) + +if TYPE_CHECKING: + import requests + + from opentelemetry.exporter.http.transport._base import BaseHTTPTransport + +_logger = logging.getLogger(__name__) +# This prevents logs generated when a log fails to be written to generate another log which fails to be written etc. etc. +_logger.addFilter(DuplicateFilter()) + + +DEFAULT_COMPRESSION = Compression.NoCompression +DEFAULT_ENDPOINT = "http://localhost:4318/" +DEFAULT_LOGS_EXPORT_PATH = "v1/logs" +DEFAULT_TIMEOUT = 10 # in seconds + + +class OTLPLogExporter(LogRecordExporter): + @overload + def __init__( + self, + endpoint: str | None = None, + certificate_file: str | None = None, + client_key_file: str | None = None, + client_certificate_file: str | None = None, + headers: Mapping[str, str] | None = None, + timeout: float | None = None, + compression: Compression | _http.Compression | None = None, + session: requests.Session | None = None, + *, + max_request_size: int | None = None, + meter_provider: MeterProvider | None = None, + ) -> None: ... + + @overload + def __init__( + self, + endpoint: str | None = None, + certificate_file: None = None, + client_key_file: None = None, + client_certificate_file: None = None, + headers: Mapping[str, str] | None = None, + timeout: float | None = None, + compression: Compression | _http.Compression | None = None, + session: requests.Session | None = None, + *, + max_request_size: int | None = None, + meter_provider: MeterProvider | None = None, + _transport: BaseHTTPTransport, + ) -> None: ... + + def __init__( + self, + endpoint: str | None = None, + certificate_file: str | None = None, + client_key_file: str | None = None, + client_certificate_file: str | None = None, + headers: Mapping[str, str] | None = None, + timeout: float | None = None, + compression: Compression | _http.Compression | None = None, + session: requests.Session | None = None, + *, + max_request_size: int | None = None, + meter_provider: MeterProvider | None = None, + _transport: BaseHTTPTransport | None = None, + ) -> None: + """OTLP HTTP log exporter. + + Args: + endpoint: Full URL of the OTLP/HTTP signal endpoint, including the signal + path. Example: ``http://collector:4318/v1/logs``. For a base URL without a + signal path, set the ``OTEL_EXPORTER_OTLP_ENDPOINT`` environment variable. + certificate_file: Path to the CA certificate file for TLS. + client_key_file: Path to the client key file for mTLS. + client_certificate_file: Path to the client certificate file for mTLS. + headers: Headers to send with each export request. + timeout: Timeout in seconds for each export request. + compression: Compression to use; one of none, gzip, deflate. + session: Requests session to use at export. + max_request_size: Maximum size in bytes of a serialized request, + measured before compression. A request exceeding this size is + dropped before being sent. Defaults to 64 MiB; a value of 0 (or + any non-positive value) disables the limit. Batch processors + group log records by count rather than serialized size, so a + batch whose serialized request exceeds this limit is dropped as + a whole and recorded as a failed export; reduce the processor's + ``max_export_batch_size`` (or raise/disable this limit) if + batches may approach it. + meter_provider: MeterProvider used for the exporter's own metrics. + """ + self._endpoint = endpoint or _resolve_endpoint(OTEL_EXPORTER_OTLP_LOGS_ENDPOINT, DEFAULT_LOGS_EXPORT_PATH) + self._compression = _normalize_compression(compression) or _resolve_compression( + OTEL_EXPORTER_OTLP_LOGS_COMPRESSION + ) + transport = _transport or _build_transport( + certificate_file, + client_key_file, + client_certificate_file, + OTEL_EXPORTER_OTLP_LOGS_CERTIFICATE, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_KEY, + OTEL_EXPORTER_OTLP_LOGS_CLIENT_CERTIFICATE, + session=session or _load_session_from_envvar(_OTEL_PYTHON_EXPORTER_OTLP_HTTP_LOGS_CREDENTIAL_PROVIDER), + ) + self._max_request_size = _DEFAULT_MAX_REQUEST_SIZE if max_request_size is None else max_request_size + self._client = _http._OTLPHTTPClient( + transport=transport, + endpoint=self._endpoint, + kind="logs", + timeout=timeout if timeout is not None else _resolve_timeout(OTEL_EXPORTER_OTLP_LOGS_TIMEOUT), + compression=self._compression, + headers=_resolve_headers(headers, OTEL_EXPORTER_OTLP_LOGS_HEADERS), + logger=_logger, + ) + self._shutdown = False + + self._metrics = create_exporter_metrics( + OtelComponentTypeValues.OTLP_HTTP_LOG_EXPORTER, + "logs", + self._endpoint, + meter_provider, + os.environ.get(OTEL_PYTHON_SDK_INTERNAL_METRICS_ENABLED, "").strip().lower() == "true", + ) + + def export(self, batch: Sequence[ReadableLogRecord]) -> LogRecordExportResult: + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring batch") + return LogRecordExportResult.FAILURE + + with self._metrics.export_operation(len(batch)) as result: + try: + serialized_data = encode_logs(batch).SerializeToString() + # pylint: disable-next=broad-exception-caught + except Exception as error: + _logger.error("Failed to encode logs batch: %s", error) + result.error = error + return LogRecordExportResult.FAILURE + + if _is_request_too_large(serialized_data, self._max_request_size): + _logger.warning( + "Dropping logs batch: serialized size %d bytes exceeds max_request_size %d bytes.", + len(serialized_data), + self._max_request_size, + ) + result.error = RequestPayloadTooLargeError( + f"Serialized logs request size {len(serialized_data)} " + f"bytes exceeds max_request_size " + f"{self._max_request_size} bytes." + ) + return LogRecordExportResult.FAILURE + + export_result = self._client.export(serialized_data) + if not export_result.success: + result.error = export_result.error + result.error_attrs = ( + {HTTP_RESPONSE_STATUS_CODE: export_result.status_code} + if export_result.status_code is not None + else None + ) + return LogRecordExportResult.FAILURE + return LogRecordExportResult.SUCCESS + + def force_flush(self, timeout_millis: int = 10_000) -> bool: + """Nothing is buffered in this exporter, so this method does nothing.""" + return True + + def shutdown(self): + if self._shutdown: + _logger.warning("Exporter already shutdown, ignoring call") + return + self._shutdown = True + self._client.shutdown() diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_deprecated_log_module.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_deprecated_log_module.py new file mode 100644 index 00000000000..07733a79bc1 --- /dev/null +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_deprecated_log_module.py @@ -0,0 +1,69 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import importlib +import sys +import unittest +from importlib.metadata import EntryPoint + +import opentelemetry.exporter.otlp.proto.http.log_exporter + +_DEPRECATED_MODULE = "opentelemetry.exporter.otlp.proto.http._log_exporter" + + +def _unload_deprecated_module() -> None: + sys.modules.pop(_DEPRECATED_MODULE, None) + sys.modules["opentelemetry.exporter.otlp.proto.http"].__dict__.pop("_log_exporter", None) + + +class TestDeprecatedLogModule(unittest.TestCase): + def setUp(self) -> None: + _unload_deprecated_module() + + def test_import_emits_deprecation_warning(self) -> None: + with self.assertWarns(DeprecationWarning) as context: + importlib.import_module(_DEPRECATED_MODULE) + message = str(context.warning) + self.assertIn("opentelemetry.exporter.otlp.proto.http.log_exporter", message) + self.assertIn("will be removed in a future release", message) + + def test_deprecation_warning_points_at_importer(self) -> None: + # pylint: disable=import-outside-toplevel,unused-import + # A literal import statement is required here since importlib.import_module + # would attribute the warning to importlib itself. + with self.assertWarns(DeprecationWarning) as context: + from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter # noqa: F401, PLC0415 + self.assertEqual(context.filename, __file__) + + def test_reexports_are_identical(self) -> None: + with self.assertWarns(DeprecationWarning): + deprecated = importlib.import_module(_DEPRECATED_MODULE) + + self.assertEqual( + deprecated.__all__, + [ + "DEFAULT_COMPRESSION", + "DEFAULT_ENDPOINT", + "DEFAULT_LOGS_EXPORT_PATH", + "DEFAULT_TIMEOUT", + "OTLPLogExporter", + ], + ) + for name in deprecated.__all__: + with self.subTest(name=name): + self.assertIs( + getattr(deprecated, name), getattr(opentelemetry.exporter.otlp.proto.http.log_exporter, name) + ) + + def test_old_entry_point_target_resolves(self) -> None: + # Entry points and configuration may still reference the old module path. + entry_point = EntryPoint( + name="log_exporter", + value="opentelemetry.exporter.otlp.proto.http._log_exporter:OTLPLogExporter", + group="test", + ) + with self.assertWarns(DeprecationWarning): + target = entry_point.load() + self.assertIs(target, opentelemetry.exporter.otlp.proto.http.log_exporter.OTLPLogExporter) diff --git a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py index 056ca99d034..fa07738d57f 100644 --- a/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py +++ b/exporter/opentelemetry-exporter-otlp-proto-http/tests/test_proto_log_exporter.py @@ -23,13 +23,13 @@ Urllib3HTTPTransport, ) from opentelemetry.exporter.otlp.common import http as _http -from opentelemetry.exporter.otlp.proto.common._log_encoder import encode_logs +from opentelemetry.exporter.otlp.proto.common.log_encoder import encode_logs from opentelemetry.exporter.otlp.proto.http import Compression from opentelemetry.exporter.otlp.proto.http._common import ( _DEFAULT_TIMEOUT, _build_transport, ) -from opentelemetry.exporter.otlp.proto.http._log_exporter import ( +from opentelemetry.exporter.otlp.proto.http.log_exporter import ( DEFAULT_ENDPOINT, DEFAULT_LOGS_EXPORT_PATH, OTLPLogExporter, @@ -65,7 +65,7 @@ from . import _mock_clock _TEST_ENDPOINT = "http://localhost:4318/v1/logs" -_LOGGER_NAME = "opentelemetry.exporter.otlp.proto.http._log_exporter" +_LOGGER_NAME = "opentelemetry.exporter.otlp.proto.http.log_exporter" _USER_AGENT = "OTel-OTLP-Exporter-Python/" + __version__ _BASE_HEADERS = { "content-type": "application/x-protobuf", @@ -392,7 +392,7 @@ def test_configuration_precedence(self): self.assertEqual(exporter._client._timeout, expected["timeout"]) @mocketize - @patch("opentelemetry.exporter.otlp.proto.http._log_exporter._build_transport") + @patch("opentelemetry.exporter.otlp.proto.http.log_exporter._build_transport") def test_custom_transport(self, mock_build_transport): Entry.single_register(Entry.POST, _TEST_ENDPOINT, status=200) custom_transport = Urllib3HTTPTransport() @@ -407,7 +407,7 @@ def test_custom_transport(self, mock_build_transport): @mocketize @patch( - "opentelemetry.exporter.otlp.proto.http._log_exporter._build_transport", + "opentelemetry.exporter.otlp.proto.http.log_exporter._build_transport", wraps=_build_transport, ) def test_certificate_args(self, mock_build_transport): @@ -612,7 +612,7 @@ def test_force_flush(self): @mocketize @patch( - "opentelemetry.exporter.otlp.proto.http._log_exporter.encode_logs", + "opentelemetry.exporter.otlp.proto.http.log_exporter.encode_logs", side_effect=ValueError("boom"), ) def test_export_encoding_failure(self, mock_encode_logs): diff --git a/exporter/opentelemetry-exporter-otlp/pyproject.toml b/exporter/opentelemetry-exporter-otlp/pyproject.toml index 2920f8b2d9b..0d0013a5b02 100644 --- a/exporter/opentelemetry-exporter-otlp/pyproject.toml +++ b/exporter/opentelemetry-exporter-otlp/pyproject.toml @@ -32,7 +32,7 @@ dependencies = [ ] [project.entry-points.opentelemetry_logs_exporter] -otlp = "opentelemetry.exporter.otlp.proto.grpc._log_exporter:OTLPLogExporter" +otlp = "opentelemetry.exporter.otlp.proto.grpc.log_exporter:OTLPLogExporter" [project.entry-points.opentelemetry_metrics_exporter] otlp = "opentelemetry.exporter.otlp.proto.grpc.metric_exporter:OTLPMetricExporter" diff --git a/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py b/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py index 315f4279975..4e333bdcc9f 100644 --- a/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py +++ b/exporter/opentelemetry-exporter-otlp/tests/test_otlp.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 -from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( +from opentelemetry.exporter.otlp.proto.grpc.log_exporter import ( OTLPLogExporter, ) from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import ( diff --git a/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py b/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py index 8daa41f7f20..0486c371764 100644 --- a/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py +++ b/opentelemetry-configuration/src/opentelemetry/configuration/_logger_provider.py @@ -92,7 +92,7 @@ def _create_otlp_http_log_exporter( from opentelemetry.exporter.otlp.proto.http import ( # noqa: PLC0415 # type: ignore[import-untyped] Compression, ) - from opentelemetry.exporter.otlp.proto.http._log_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] + from opentelemetry.exporter.otlp.proto.http.log_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] OTLPLogExporter, ) except ImportError as exc: @@ -121,7 +121,7 @@ def _create_otlp_grpc_log_exporter( # pylint: disable=import-outside-toplevel,no-name-in-module import grpc # noqa: PLC0415 # type: ignore[import-untyped] - from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] + from opentelemetry.exporter.otlp.proto.grpc.log_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] OTLPLogExporter, ) except ImportError as exc: @@ -148,7 +148,7 @@ def _create_otlp_file_development_log_exporter( """Create an OTLP file (JSON Lines) log exporter from config.""" try: # pylint: disable=import-outside-toplevel,no-name-in-module - from opentelemetry.exporter.otlp.json.file._log_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] + from opentelemetry.exporter.otlp.json.file.log_exporter import ( # noqa: PLC0415 # type: ignore[import-untyped] FileLogExporter, ) except ImportError as exc: diff --git a/opentelemetry-configuration/tests/test_logger_provider.py b/opentelemetry-configuration/tests/test_logger_provider.py index 936fbe32f7e..e3b35774272 100644 --- a/opentelemetry-configuration/tests/test_logger_provider.py +++ b/opentelemetry-configuration/tests/test_logger_provider.py @@ -235,7 +235,7 @@ def test_otlp_http_missing_package_raises(self): sys.modules, { "opentelemetry.exporter.otlp.proto.http": None, - "opentelemetry.exporter.otlp.proto.http._log_exporter": None, + "opentelemetry.exporter.otlp.proto.http.log_exporter": None, }, ), self.assertRaises(ConfigurationError), @@ -249,7 +249,7 @@ def test_otlp_grpc_missing_package_raises(self): sys.modules, { "grpc": None, - "opentelemetry.exporter.otlp.proto.grpc._log_exporter": None, + "opentelemetry.exporter.otlp.proto.grpc.log_exporter": None, }, ), self.assertRaises(ConfigurationError), @@ -262,7 +262,7 @@ def test_otlp_file_development_missing_package_raises(self): patch.dict( sys.modules, { - "opentelemetry.exporter.otlp.json.file._log_exporter": None, + "opentelemetry.exporter.otlp.json.file.log_exporter": None, }, ), self.assertRaises(ConfigurationError) as ctx, @@ -278,7 +278,7 @@ def test_otlp_file_development_default_stdout(self): with patch.dict( sys.modules, { - "opentelemetry.exporter.otlp.json.file._log_exporter": mock_module, + "opentelemetry.exporter.otlp.json.file.log_exporter": mock_module, }, ): config = LogRecordExporterConfig(otlp_file_development=ExperimentalOtlpFileExporterConfig()) @@ -296,7 +296,7 @@ def test_otlp_file_development_file_uri(self): with patch.dict( sys.modules, { - "opentelemetry.exporter.otlp.json.file._log_exporter": mock_module, + "opentelemetry.exporter.otlp.json.file.log_exporter": mock_module, }, ): config = LogRecordExporterConfig( @@ -315,7 +315,7 @@ def test_otlp_file_development_unsupported_output_stream_raises(self): with patch.dict( sys.modules, { - "opentelemetry.exporter.otlp.json.file._log_exporter": mock_module, + "opentelemetry.exporter.otlp.json.file.log_exporter": mock_module, }, ): config = LogRecordExporterConfig( @@ -340,7 +340,7 @@ def test_otlp_http_exporter_endpoint(self): sys.modules, { "opentelemetry.exporter.otlp.proto.http": mock_module, - "opentelemetry.exporter.otlp.proto.http._log_exporter": mock_log_module, + "opentelemetry.exporter.otlp.proto.http.log_exporter": mock_log_module, }, ): config = LogRecordExporterConfig( @@ -368,7 +368,7 @@ def test_otlp_http_exporter_headers(self): sys.modules, { "opentelemetry.exporter.otlp.proto.http": mock_module, - "opentelemetry.exporter.otlp.proto.http._log_exporter": mock_log_module, + "opentelemetry.exporter.otlp.proto.http.log_exporter": mock_log_module, }, ): config = LogRecordExporterConfig( @@ -392,7 +392,7 @@ def test_otlp_http_exporter_deflate_compression(self): sys.modules, { "opentelemetry.exporter.otlp.proto.http": mock_module, - "opentelemetry.exporter.otlp.proto.http._log_exporter": mock_log_module, + "opentelemetry.exporter.otlp.proto.http.log_exporter": mock_log_module, }, ): config = LogRecordExporterConfig(otlp_http=OtlpHttpExporterConfig(compression="deflate")) @@ -412,7 +412,7 @@ def test_otlp_grpc_exporter_endpoint(self): sys.modules, { "grpc": mock_grpc, - "opentelemetry.exporter.otlp.proto.grpc._log_exporter": mock_grpc_log_module, + "opentelemetry.exporter.otlp.proto.grpc.log_exporter": mock_grpc_log_module, }, ): config = LogRecordExporterConfig( diff --git a/rationale.md b/rationale.md index 9c10727fd59..b659752c5a3 100644 --- a/rationale.md +++ b/rationale.md @@ -66,3 +66,10 @@ Purely for illustration purposes, not intended to represent actual releases: - Contains the experimental public API for logging plus other unstable features. There are no stability guarantees. - `opentelemetry-sdk` 1.x.yrc1 - Contains the experimental public SDK for logging plus other unstable features. There are no stability guarantees. + +#### V1.46.0 Release (with logs) + +- `opentelemetry-api` 1.46.0 + - Contains APIs for tracing, baggage, propagators, context, metrics, and logs +- `opentelemetry-sdk` 1.46.0 + - Contains SDK components for tracing, baggage, propagators, context, metrics, and logs diff --git a/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_logs_functional.py b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_logs_functional.py index d895fec723c..54e0387758f 100644 --- a/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_logs_functional.py +++ b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_logs_functional.py @@ -19,19 +19,19 @@ from opentelemetry.exporter.otlp.common.http import ( Compression as JSONCompression, ) -from opentelemetry.exporter.otlp.json.file._log_exporter import ( +from opentelemetry.exporter.otlp.json.file.log_exporter import ( FileLogExporter, ) -from opentelemetry.exporter.otlp.json.http._log_exporter import ( +from opentelemetry.exporter.otlp.json.http.log_exporter import ( OTLPLogExporter as JSONLogExporter, ) -from opentelemetry.exporter.otlp.proto.grpc._log_exporter import ( +from opentelemetry.exporter.otlp.proto.grpc.log_exporter import ( OTLPLogExporter as GRPCLogExporter, ) from opentelemetry.exporter.otlp.proto.http import ( Compression as HTTPCompression, ) -from opentelemetry.exporter.otlp.proto.http._log_exporter import ( +from opentelemetry.exporter.otlp.proto.http.log_exporter import ( OTLPLogExporter as HTTPLogExporter, ) from opentelemetry.logs import Logger, SeverityNumber diff --git a/tests/opentelemetry-test-utils/tests/test_otlp_test_server.py b/tests/opentelemetry-test-utils/tests/test_otlp_test_server.py index 8e8df87b214..37656619543 100644 --- a/tests/opentelemetry-test-utils/tests/test_otlp_test_server.py +++ b/tests/opentelemetry-test-utils/tests/test_otlp_test_server.py @@ -27,7 +27,7 @@ # pylint: disable-next=unused-import import opentelemetry.proto # noqa: F401 from opentelemetry.exporter.otlp.proto.http import Compression - from opentelemetry.exporter.otlp.proto.http._log_exporter import ( + from opentelemetry.exporter.otlp.proto.http.log_exporter import ( OTLPLogExporter, ) from opentelemetry.exporter.otlp.proto.http.metric_exporter import ( From 0109addde2d8b1a7886a9773f0632aa523ec3050 Mon Sep 17 00:00:00 2001 From: Lukas Hering Date: Fri, 2 Oct 2026 23:13:29 -0400 Subject: [PATCH 2/2] add changelog fragments --- .changelog/5733.added | 1 + .changelog/5733.changed | 1 + .changelog/5733.changed.1 | 1 + .changelog/5733.deprecated | 1 + .changelog/5733.deprecated.1 | 1 + 5 files changed, 5 insertions(+) create mode 100644 .changelog/5733.added create mode 100644 .changelog/5733.changed create mode 100644 .changelog/5733.changed.1 create mode 100644 .changelog/5733.deprecated create mode 100644 .changelog/5733.deprecated.1 diff --git a/.changelog/5733.added b/.changelog/5733.added new file mode 100644 index 00000000000..cd3eb79e09b --- /dev/null +++ b/.changelog/5733.added @@ -0,0 +1 @@ +`opentelemetry-exporter-otlp-json-http`, `opentelemetry-exporter-otlp-json-file`: export the log exporter at the package top level diff --git a/.changelog/5733.changed b/.changelog/5733.changed new file mode 100644 index 00000000000..75283e9d91e --- /dev/null +++ b/.changelog/5733.changed @@ -0,0 +1 @@ +`opentelemetry-exporter-otlp-proto-grpc`, `opentelemetry-exporter-otlp-proto-http`, `opentelemetry-exporter-otlp-json-http`, `opentelemetry-exporter-otlp-json-file`: stabilize the log exporter as the public `log_exporter` module diff --git a/.changelog/5733.changed.1 b/.changelog/5733.changed.1 new file mode 100644 index 00000000000..783f49a8978 --- /dev/null +++ b/.changelog/5733.changed.1 @@ -0,0 +1 @@ +`opentelemetry-exporter-otlp-proto-common`, `opentelemetry-exporter-otlp-json-common`: stabilize the log encoder as the public `log_encoder` module diff --git a/.changelog/5733.deprecated b/.changelog/5733.deprecated new file mode 100644 index 00000000000..d61bea16438 --- /dev/null +++ b/.changelog/5733.deprecated @@ -0,0 +1 @@ +`opentelemetry-exporter-otlp-proto-grpc`, `opentelemetry-exporter-otlp-proto-http`, `opentelemetry-exporter-otlp-json-http`, `opentelemetry-exporter-otlp-json-file`: deprecate the `_log_exporter` module in favor of `log_exporter` diff --git a/.changelog/5733.deprecated.1 b/.changelog/5733.deprecated.1 new file mode 100644 index 00000000000..04b73997167 --- /dev/null +++ b/.changelog/5733.deprecated.1 @@ -0,0 +1 @@ + `opentelemetry-exporter-otlp-proto-common`, `opentelemetry-exporter-otlp-json-common`: deprecate the `_log_encoder` module in favor of `log_encoder`