Skip to content

Commit 2d591e4

Browse files
author
Ron Yishai
authored
Adding support for setting OTLP exporter protocol by env vars (#2893)
1 parent 71c1148 commit 2d591e4

File tree

4 files changed

+169
-25
lines changed

4 files changed

+169
-25
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1515
([#2870](https://github.com/open-telemetry/opentelemetry-python/pull/2870))
1616
- Fix: Remove `LogEmitter.flush()` to align with OTel Log spec
1717
([#2863](https://github.com/open-telemetry/opentelemetry-python/pull/2863))
18+
- Add support for setting OTLP export protocol with env vars, as defined in the
19+
[specifications](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#specify-protocol)
20+
([#2893](https://github.com/open-telemetry/opentelemetry-python/pull/2893))
1821

1922
## [1.12.0-0.33b0](https://github.com/open-telemetry/opentelemetry-python/releases/tag/v1.12.0) - 2022-08-08
2023

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py

Lines changed: 81 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from typing import Dict, Optional, Sequence, Tuple, Type
2525

2626
from pkg_resources import iter_entry_points
27+
from typing_extensions import Literal
2728

2829
from opentelemetry.environment_variables import (
2930
OTEL_LOGS_EXPORTER,
@@ -40,6 +41,10 @@
4041
from opentelemetry.sdk._logs.export import BatchLogProcessor, LogExporter
4142
from opentelemetry.sdk.environment_variables import (
4243
_OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED,
44+
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
45+
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
46+
OTEL_EXPORTER_OTLP_PROTOCOL,
47+
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
4348
)
4449
from opentelemetry.sdk.metrics import MeterProvider
4550
from opentelemetry.sdk.metrics.export import (
@@ -55,26 +60,91 @@
5560

5661
_EXPORTER_OTLP = "otlp"
5762
_EXPORTER_OTLP_PROTO_GRPC = "otlp_proto_grpc"
63+
_EXPORTER_OTLP_PROTO_HTTP = "otlp_proto_http"
64+
65+
_EXPORTER_BY_OTLP_PROTOCOL = {
66+
"grpc": _EXPORTER_OTLP_PROTO_GRPC,
67+
"http/protobuf": _EXPORTER_OTLP_PROTO_HTTP,
68+
}
69+
70+
_EXPORTER_ENV_BY_SIGNAL_TYPE = {
71+
"traces": OTEL_TRACES_EXPORTER,
72+
"metrics": OTEL_METRICS_EXPORTER,
73+
"logs": OTEL_LOGS_EXPORTER,
74+
}
75+
76+
_PROTOCOL_ENV_BY_SIGNAL_TYPE = {
77+
"traces": OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
78+
"metrics": OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
79+
"logs": OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
80+
}
5881

5982
_RANDOM_ID_GENERATOR = "random"
6083
_DEFAULT_ID_GENERATOR = _RANDOM_ID_GENERATOR
6184

85+
_logger = logging.getLogger(__name__)
86+
6287

6388
def _get_id_generator() -> str:
6489
return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR)
6590

6691

67-
def _get_exporter_names(names: str) -> Sequence[str]:
68-
exporters = set()
92+
def _get_exporter_entry_point(
93+
exporter_name: str, signal_type: Literal["traces", "metrics", "logs"]
94+
):
95+
if exporter_name not in (
96+
_EXPORTER_OTLP,
97+
_EXPORTER_OTLP_PROTO_GRPC,
98+
_EXPORTER_OTLP_PROTO_HTTP,
99+
):
100+
return exporter_name
101+
102+
# Checking env vars for OTLP protocol (grpc/http).
103+
otlp_protocol = environ.get(
104+
_PROTOCOL_ENV_BY_SIGNAL_TYPE[signal_type]
105+
) or environ.get(OTEL_EXPORTER_OTLP_PROTOCOL)
106+
107+
if not otlp_protocol:
108+
if exporter_name == _EXPORTER_OTLP:
109+
return _EXPORTER_OTLP_PROTO_GRPC
110+
return exporter_name
111+
112+
otlp_protocol = otlp_protocol.strip()
113+
114+
if exporter_name == _EXPORTER_OTLP:
115+
if otlp_protocol not in _EXPORTER_BY_OTLP_PROTOCOL:
116+
# Invalid value was set by the env var
117+
raise RuntimeError(
118+
f"Unsupported OTLP protocol '{otlp_protocol}' is configured"
119+
)
120+
121+
return _EXPORTER_BY_OTLP_PROTOCOL[otlp_protocol]
122+
123+
# grpc/http already specified by exporter_name, only add a warning in case
124+
# of a conflict.
125+
exporter_name_by_env = _EXPORTER_BY_OTLP_PROTOCOL.get(otlp_protocol)
126+
if exporter_name_by_env and exporter_name != exporter_name_by_env:
127+
_logger.warning(
128+
"Conflicting values for %s OTLP exporter protocol, using '%s'",
129+
signal_type,
130+
exporter_name,
131+
)
132+
133+
return exporter_name
134+
69135

70-
if names and names.lower().strip() != "none":
71-
exporters.update({_exporter.strip() for _exporter in names.split(",")})
136+
def _get_exporter_names(
137+
signal_type: Literal["traces", "metrics", "logs"]
138+
) -> Sequence[str]:
139+
names = environ.get(_EXPORTER_ENV_BY_SIGNAL_TYPE.get(signal_type, ""))
72140

73-
if _EXPORTER_OTLP in exporters:
74-
exporters.remove(_EXPORTER_OTLP)
75-
exporters.add(_EXPORTER_OTLP_PROTO_GRPC)
141+
if not names or names.lower().strip() == "none":
142+
return []
76143

77-
return list(exporters)
144+
return [
145+
_get_exporter_entry_point(_exporter.strip(), signal_type)
146+
for _exporter in names.split(",")
147+
]
78148

79149

80150
def _init_tracing(
@@ -232,9 +302,9 @@ def _import_id_generator(id_generator_name: str) -> IdGenerator:
232302

233303
def _initialize_components(auto_instrumentation_version):
234304
trace_exporters, metric_exporters, log_exporters = _import_exporters(
235-
_get_exporter_names(environ.get(OTEL_TRACES_EXPORTER)),
236-
_get_exporter_names(environ.get(OTEL_METRICS_EXPORTER)),
237-
_get_exporter_names(environ.get(OTEL_LOGS_EXPORTER)),
305+
_get_exporter_names("traces"),
306+
_get_exporter_names("metrics"),
307+
_get_exporter_names("logs"),
238308
)
239309
id_generator_name = _get_id_generator()
240310
id_generator = _import_id_generator(id_generator_name)

opentelemetry-sdk/src/opentelemetry/sdk/environment_variables.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,27 @@
234234
OTLP exporter.
235235
"""
236236

237+
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"
238+
"""
239+
.. envvar:: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL
240+
241+
The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for spans.
242+
"""
243+
244+
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL = "OTEL_EXPORTER_OTLP_METRICS_PROTOCOL"
245+
"""
246+
.. envvar:: OTEL_EXPORTER_OTLP_METRICS_PROTOCOL
247+
248+
The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for metrics.
249+
"""
250+
251+
OTEL_EXPORTER_OTLP_LOGS_PROTOCOL = "OTEL_EXPORTER_OTLP_LOGS_PROTOCOL"
252+
"""
253+
.. envvar:: OTEL_EXPORTER_OTLP_LOGS_PROTOCOL
254+
255+
The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for logs.
256+
"""
257+
237258
OTEL_EXPORTER_OTLP_CERTIFICATE = "OTEL_EXPORTER_OTLP_CERTIFICATE"
238259
"""
239260
.. envvar:: OTEL_EXPORTER_OTLP_CERTIFICATE
@@ -314,13 +335,6 @@
314335
A scheme of https indicates a secure connection and takes precedence over this configuration setting.
315336
"""
316337

317-
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL = "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL"
318-
"""
319-
.. envvar:: OTEL_EXPORTER_OTLP_TRACES_PROTOCOL
320-
321-
The :envvar:`OTEL_EXPORTER_OTLP_TRACES_PROTOCOL` represents the the transport protocol for spans.
322-
"""
323-
324338
OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE = "OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE"
325339
"""
326340
.. envvar:: OTEL_EXPORTER_OTLP_TRACES_CERTIFICATE

opentelemetry-sdk/tests/test_configurator.py

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from opentelemetry.sdk._configuration import (
2626
_EXPORTER_OTLP,
2727
_EXPORTER_OTLP_PROTO_GRPC,
28+
_EXPORTER_OTLP_PROTO_HTTP,
2829
_get_exporter_names,
2930
_get_id_generator,
3031
_import_exporters,
@@ -413,25 +414,81 @@ def test_metrics_init_exporter(self):
413414

414415

415416
class TestExporterNames(TestCase):
416-
def test_otlp_exporter_overwrite(self):
417-
for exporter in [_EXPORTER_OTLP, _EXPORTER_OTLP_PROTO_GRPC]:
417+
@patch.dict(
418+
environ,
419+
{
420+
"OTEL_TRACES_EXPORTER": _EXPORTER_OTLP,
421+
"OTEL_METRICS_EXPORTER": _EXPORTER_OTLP_PROTO_GRPC,
422+
"OTEL_LOGS_EXPORTER": _EXPORTER_OTLP_PROTO_HTTP,
423+
},
424+
)
425+
def test_otlp_exporter(self):
426+
self.assertEqual(
427+
_get_exporter_names("traces"), [_EXPORTER_OTLP_PROTO_GRPC]
428+
)
429+
self.assertEqual(
430+
_get_exporter_names("metrics"), [_EXPORTER_OTLP_PROTO_GRPC]
431+
)
432+
self.assertEqual(
433+
_get_exporter_names("logs"), [_EXPORTER_OTLP_PROTO_HTTP]
434+
)
435+
436+
@patch.dict(
437+
environ,
438+
{
439+
"OTEL_TRACES_EXPORTER": _EXPORTER_OTLP,
440+
"OTEL_METRICS_EXPORTER": _EXPORTER_OTLP,
441+
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/protobuf",
442+
"OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": "grpc",
443+
},
444+
)
445+
def test_otlp_custom_exporter(self):
446+
self.assertEqual(
447+
_get_exporter_names("traces"), [_EXPORTER_OTLP_PROTO_HTTP]
448+
)
449+
self.assertEqual(
450+
_get_exporter_names("metrics"), [_EXPORTER_OTLP_PROTO_GRPC]
451+
)
452+
453+
@patch.dict(
454+
environ,
455+
{
456+
"OTEL_TRACES_EXPORTER": _EXPORTER_OTLP_PROTO_HTTP,
457+
"OTEL_METRICS_EXPORTER": _EXPORTER_OTLP_PROTO_GRPC,
458+
"OTEL_EXPORTER_OTLP_PROTOCOL": "grpc",
459+
"OTEL_EXPORTER_OTLP_METRICS_PROTOCOL": "http/protobuf",
460+
},
461+
)
462+
def test_otlp_exporter_conflict(self):
463+
# Verify that OTEL_*_EXPORTER is used, and a warning is logged
464+
with self.assertLogs(level="WARNING") as logs_context:
465+
self.assertEqual(
466+
_get_exporter_names("traces"), [_EXPORTER_OTLP_PROTO_HTTP]
467+
)
468+
assert len(logs_context.output) == 1
469+
470+
with self.assertLogs(level="WARNING") as logs_context:
418471
self.assertEqual(
419-
_get_exporter_names(exporter), [_EXPORTER_OTLP_PROTO_GRPC]
472+
_get_exporter_names("metrics"), [_EXPORTER_OTLP_PROTO_GRPC]
420473
)
474+
assert len(logs_context.output) == 1
421475

476+
@patch.dict(environ, {"OTEL_TRACES_EXPORTER": "jaeger,zipkin"})
422477
def test_multiple_exporters(self):
423478
self.assertEqual(
424-
sorted(_get_exporter_names("jaeger,zipkin")), ["jaeger", "zipkin"]
479+
sorted(_get_exporter_names("traces")), ["jaeger", "zipkin"]
425480
)
426481

482+
@patch.dict(environ, {"OTEL_TRACES_EXPORTER": "none"})
427483
def test_none_exporters(self):
428-
self.assertEqual(sorted(_get_exporter_names("none")), [])
484+
self.assertEqual(sorted(_get_exporter_names("traces")), [])
429485

430486
def test_no_exporters(self):
431-
self.assertEqual(sorted(_get_exporter_names(None)), [])
487+
self.assertEqual(sorted(_get_exporter_names("traces")), [])
432488

489+
@patch.dict(environ, {"OTEL_TRACES_EXPORTER": ""})
433490
def test_empty_exporters(self):
434-
self.assertEqual(sorted(_get_exporter_names("")), [])
491+
self.assertEqual(sorted(_get_exporter_names("traces")), [])
435492

436493

437494
class TestImportExporters(TestCase):

0 commit comments

Comments
 (0)