diff --git a/CHANGELOG.md b/CHANGELOG.md index a4af2ef82ec..307d9ae91ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#2870](https://github.com/open-telemetry/opentelemetry-python/pull/2870)) - Fix: Remove `LogEmitter.flush()` to align with OTel Log spec ([#2863](https://github.com/open-telemetry/opentelemetry-python/pull/2863)) +- Add support for setting OTLP export protocol with env vars, as defined in the + [specifications](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specify-protocol) + ([#2893](https://github.com/open-telemetry/opentelemetry-python/pull/2893)) ## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0) - 2022-08-08 diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 25a6580f1ba..159d471900a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -24,6 +24,7 @@ from typing import Dict, Optional, Sequence, Tuple, Type from pkg_resources import iter_entry_points +from typing_extensions import Literal from opentelemetry.environment_variables import ( OTEL_LOGS_EXPORTER, @@ -40,6 +41,10 @@ from opentelemetry.sdk._logs.export import BatchLogProcessor, LogExporter from opentelemetry.sdk.environment_variables import ( _OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED, + OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, + OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, + OTEL_EXPORTER_OTLP_PROTOCOL, + OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, ) from opentelemetry.sdk.metrics import MeterProvider from opentelemetry.sdk.metrics.export import ( @@ -55,26 +60,91 @@ _EXPORTER_OTLP = "otlp" _EXPORTER_OTLP_PROTO_GRPC = "otlp_proto_grpc" +_EXPORTER_OTLP_PROTO_HTTP = "otlp_proto_http" + +_EXPORTER_BY_OTLP_PROTOCOL = { + "grpc": _EXPORTER_OTLP_PROTO_GRPC, + "http/protobuf": _EXPORTER_OTLP_PROTO_HTTP, +} + +_EXPORTER_ENV_BY_SIGNAL_TYPE = { + "traces": OTEL_TRACES_EXPORTER, + "metrics": OTEL_METRICS_EXPORTER, + "logs": OTEL_LOGS_EXPORTER, +} + +_PROTOCOL_ENV_BY_SIGNAL_TYPE = { + "traces": OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, + "metrics": OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, + "logs": OTEL_EXPORTER_OTLP_LOGS_PROTOCOL, +} _RANDOM_ID_GENERATOR = "random" _DEFAULT_ID_GENERATOR = _RANDOM_ID_GENERATOR +_logger = logging.getLogger(__name__) + def _get_id_generator() -> str: return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR) -def _get_exporter_names(names: str) -> Sequence[str]: - exporters = set() +def _get_exporter_entry_point( + exporter_name: str, signal_type: Literal["traces", "metrics", "logs"] +): + if exporter_name not in ( + _EXPORTER_OTLP, + _EXPORTER_OTLP_PROTO_GRPC, + _EXPORTER_OTLP_PROTO_HTTP, + ): + return exporter_name + + # Checking env vars for OTLP protocol (grpc/http). + otlp_protocol = environ.get( + _PROTOCOL_ENV_BY_SIGNAL_TYPE[signal_type] + ) or environ.get(OTEL_EXPORTER_OTLP_PROTOCOL) + + if not otlp_protocol: + if exporter_name == _EXPORTER_OTLP: + return _EXPORTER_OTLP_PROTO_GRPC + return exporter_name + + otlp_protocol = otlp_protocol.strip() + + if exporter_name == _EXPORTER_OTLP: + if otlp_protocol not in _EXPORTER_BY_OTLP_PROTOCOL: + # Invalid value was set by the env var + raise RuntimeError( + f"Unsupported OTLP protocol '{otlp_protocol}' is configured" + ) + + return _EXPORTER_BY_OTLP_PROTOCOL[otlp_protocol] + + # grpc/http already specified by exporter_name, only add a warning in case + # of a conflict. + exporter_name_by_env = _EXPORTER_BY_OTLP_PROTOCOL.get(otlp_protocol) + if exporter_name_by_env and exporter_name != exporter_name_by_env: + _logger.warning( + "Conflicting values for %s OTLP exporter protocol, using '%s'", + signal_type, + exporter_name, + ) + + return exporter_name + - if names and names.lower().strip() != "none": - exporters.update({_exporter.strip() for _exporter in names.split(",")}) +def _get_exporter_names( + signal_type: Literal["traces", "metrics", "logs"] +) -> Sequence[str]: + names = environ.get(_EXPORTER_ENV_BY_SIGNAL_TYPE.get(signal_type, "")) - if _EXPORTER_OTLP in exporters: - exporters.remove(_EXPORTER_OTLP) - exporters.add(_EXPORTER_OTLP_PROTO_GRPC) + if not names or names.lower().strip() == "none": + return [] - return list(exporters) + return [ + _get_exporter_entry_point(_exporter.strip(), signal_type) + for _exporter in names.split(",") + ] def _init_tracing( @@ -232,9 +302,9 @@ def _import_id_generator(id_generator_name: str) -> IdGenerator: def _initialize_components(auto_instrumentation_version): trace_exporters, metric_exporters, log_exporters = _import_exporters( - _get_exporter_names(environ.get(OTEL_TRACES_EXPORTER)), - _get_exporter_names(environ.get(OTEL_METRICS_EXPORTER)), - _get_exporter_names(environ.get(OTEL_LOGS_EXPORTER)), + _get_exporter_names("traces"), + _get_exporter_names("metrics"), + _get_exporter_names("logs"), ) id_generator_name = _get_id_generator() id_generator = _import_id_generator(id_generator_name) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py index 384d563d259..ad635e89d58 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py @@ -234,6 +234,27 @@ OTLP exporter. """ +OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" +""" +.. envvar:: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL + +The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for spans. +""" + +OTEL_EXPORTER_OTLP_METRICS_PROTOCOL = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL" +""" +.. envvar:: OTEL_EXPORTER_OTLP_METRICS_PROTOCOL + +The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for metrics. +""" + +OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL" +""" +.. envvar:: OTEL_EXPORTER_OTLP_LOGS_PROTOCOL + +The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for logs. +""" + OTEL_EXPORTER_OTLP_CERTIFICATE = "OTEL_EXPORTER_OTLP_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_CERTIFICATE @@ -314,13 +335,6 @@ A scheme of https indicates a secure connection and takes precedence over this configuration setting. """ -OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL" -""" -.. envvar:: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL - -The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for spans. -""" - OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE = "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE" """ .. envvar:: OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 6afbf4d2953..4aae8aa53be 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -25,6 +25,7 @@ from opentelemetry.sdk._configuration import ( _EXPORTER_OTLP, _EXPORTER_OTLP_PROTO_GRPC, + _EXPORTER_OTLP_PROTO_HTTP, _get_exporter_names, _get_id_generator, _import_exporters, @@ -413,25 +414,81 @@ def test_metrics_init_exporter(self): class TestExporterNames(TestCase): - def test_otlp_exporter_overwrite(self): - for exporter in [_EXPORTER_OTLP, _EXPORTER_OTLP_PROTO_GRPC]: + @patch.dict( + environ, + { + "OTEL_TRACES_EXPORTER": _EXPORTER_OTLP, + "OTEL_METRICS_EXPORTER": _EXPORTER_OTLP_PROTO_GRPC, + "OTEL_LOGS_EXPORTER": _EXPORTER_OTLP_PROTO_HTTP, + }, + ) + def test_otlp_exporter(self): + self.assertEqual( + _get_exporter_names("traces"), [_EXPORTER_OTLP_PROTO_GRPC] + ) + self.assertEqual( + _get_exporter_names("metrics"), [_EXPORTER_OTLP_PROTO_GRPC] + ) + self.assertEqual( + _get_exporter_names("logs"), [_EXPORTER_OTLP_PROTO_HTTP] + ) + + @patch.dict( + environ, + { + "OTEL_TRACES_EXPORTER": _EXPORTER_OTLP, + "OTEL_METRICS_EXPORTER": _EXPORTER_OTLP, + "OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf", + "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": "grpc", + }, + ) + def test_otlp_custom_exporter(self): + self.assertEqual( + _get_exporter_names("traces"), [_EXPORTER_OTLP_PROTO_HTTP] + ) + self.assertEqual( + _get_exporter_names("metrics"), [_EXPORTER_OTLP_PROTO_GRPC] + ) + + @patch.dict( + environ, + { + "OTEL_TRACES_EXPORTER": _EXPORTER_OTLP_PROTO_HTTP, + "OTEL_METRICS_EXPORTER": _EXPORTER_OTLP_PROTO_GRPC, + "OTEL_EXPORTER_OTLP_PROTOCOL": "grpc", + "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": "http/protobuf", + }, + ) + def test_otlp_exporter_conflict(self): + # Verify that OTEL_*_EXPORTER is used, and a warning is logged + with self.assertLogs(level="WARNING") as logs_context: + self.assertEqual( + _get_exporter_names("traces"), [_EXPORTER_OTLP_PROTO_HTTP] + ) + assert len(logs_context.output) == 1 + + with self.assertLogs(level="WARNING") as logs_context: self.assertEqual( - _get_exporter_names(exporter), [_EXPORTER_OTLP_PROTO_GRPC] + _get_exporter_names("metrics"), [_EXPORTER_OTLP_PROTO_GRPC] ) + assert len(logs_context.output) == 1 + @patch.dict(environ, {"OTEL_TRACES_EXPORTER": "jaeger,zipkin"}) def test_multiple_exporters(self): self.assertEqual( - sorted(_get_exporter_names("jaeger,zipkin")), ["jaeger", "zipkin"] + sorted(_get_exporter_names("traces")), ["jaeger", "zipkin"] ) + @patch.dict(environ, {"OTEL_TRACES_EXPORTER": "none"}) def test_none_exporters(self): - self.assertEqual(sorted(_get_exporter_names("none")), []) + self.assertEqual(sorted(_get_exporter_names("traces")), []) def test_no_exporters(self): - self.assertEqual(sorted(_get_exporter_names(None)), []) + self.assertEqual(sorted(_get_exporter_names("traces")), []) + @patch.dict(environ, {"OTEL_TRACES_EXPORTER": ""}) def test_empty_exporters(self): - self.assertEqual(sorted(_get_exporter_names("")), []) + self.assertEqual(sorted(_get_exporter_names("traces")), []) class TestImportExporters(TestCase):