Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 28 additions & 13 deletions src/elasticotel/distro/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

import logging
import os
from typing import Any
from urllib.parse import urlparse, urlunparse

from opentelemetry._opamp.agent import OpAMPAgent
Expand All @@ -27,10 +28,6 @@
OTEL_METRICS_EXPORTER,
OTEL_TRACES_EXPORTER,
)
from opentelemetry.exporter.otlp.proto.grpc import _USER_AGENT_HEADER_VALUE
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter as GRPCOTLPLogExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import OTLPMetricExporter as GRPCOTLPMetricExporter
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as GRPCOTLPSpanExporter
from opentelemetry.exporter.otlp.proto.http import _OTLP_HTTP_HEADERS
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter as HTTPOTLPLogExporter
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as HTTPOTLPMetricExporter
Expand Down Expand Up @@ -96,19 +93,37 @@ def _http_exporter_options(signal_headers_env_var: str) -> dict[str, dict[str, s
class ElasticOpenTelemetryConfigurator(_OTelSDKConfigurator):
def _configure(self, **kwargs):
# override GRPC and HTTP user agent headers
otlp_grpc_exporter_options = {
"channel_options": (
("grpc.primary_user_agent", f"{EDOT_GRPC_USER_AGENT_HEADER_VALUE} {_USER_AGENT_HEADER_VALUE}"),
)
}
kwargs["exporter_args_map"] = {
GRPCOTLPLogExporter: otlp_grpc_exporter_options,
GRPCOTLPMetricExporter: otlp_grpc_exporter_options,
GRPCOTLPSpanExporter: otlp_grpc_exporter_options,
exporter_args_map: dict[type, dict[str, Any]] = {
HTTPOTLPLogExporter: _http_exporter_options(OTEL_EXPORTER_OTLP_LOGS_HEADERS),
HTTPOTLPMetricExporter: _http_exporter_options(OTEL_EXPORTER_OTLP_METRICS_HEADERS),
HTTPOTLPSpanExporter: _http_exporter_options(OTEL_EXPORTER_OTLP_TRACES_HEADERS),
}
# When running in the k8s operator the bundled grpcio may not be built for the application's Python
# version (e.g. cp312 wheels on Python 3.13) and grpc fails to import. The operator exports via
# http/protobuf so we don't want to fail if we can't import the grpc exporters.
try:
from opentelemetry.exporter.otlp.proto.grpc import _USER_AGENT_HEADER_VALUE
from opentelemetry.exporter.otlp.proto.grpc._log_exporter import OTLPLogExporter as GRPCOTLPLogExporter
from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
OTLPMetricExporter as GRPCOTLPMetricExporter,
)
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter as GRPCOTLPSpanExporter
except ImportError:
pass
else:
otlp_grpc_exporter_options = {
"channel_options": (
("grpc.primary_user_agent", f"{EDOT_GRPC_USER_AGENT_HEADER_VALUE} {_USER_AGENT_HEADER_VALUE}"),
)
}
exporter_args_map.update(
{
GRPCOTLPLogExporter: otlp_grpc_exporter_options,
GRPCOTLPMetricExporter: otlp_grpc_exporter_options,
GRPCOTLPSpanExporter: otlp_grpc_exporter_options,
}
)
kwargs["exporter_args_map"] = exporter_args_map

super()._configure(**kwargs)

Expand Down
39 changes: 38 additions & 1 deletion tests/distro/test_distro.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,11 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import importlib
import json
import logging
import os
import sys
from unittest import TestCase, mock, skipIf

from opentelemetry import trace
Expand All @@ -26,6 +28,9 @@
OTEL_METRICS_EXPORTER,
OTEL_TRACES_EXPORTER,
)
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter as HTTPOTLPLogExporter
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter as HTTPOTLPMetricExporter
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter as HTTPOTLPSpanExporter

from elasticotel.distro import ElasticOpenTelemetryConfigurator, ElasticOpenTelemetryDistro
from elasticotel.distro import logger as distro_logger
Expand All @@ -48,6 +53,7 @@
from opentelemetry.instrumentation.logging.handler import LoggingHandler
except ImportError:
LoggingHandler = None
from opentelemetry.sdk._configuration import _initialize_components
from opentelemetry.sdk._logs import LoggingHandler as SDKLoggingHandler
from opentelemetry.sdk.environment_variables import (
OTEL_EXPERIMENTAL_RESOURCE_DETECTORS,
Expand All @@ -58,7 +64,7 @@
OTEL_TRACES_SAMPLER,
OTEL_TRACES_SAMPLER_ARG,
)
from opentelemetry.sdk.trace import _TracerConfig, sampling
from opentelemetry.sdk.trace import TracerProvider, _TracerConfig, sampling
from opentelemetry.sdk.util.instrumentation import _scope_name_matches_glob
from opentelemetry.util._once import Once

Expand Down Expand Up @@ -430,6 +436,37 @@ def test_configurator_does_not_log_otel_env_vars_by_default(self, logger_mock):

logger_mock.assert_not_called()

@mock.patch.dict("os.environ", {OTEL_EXPORTER_OTLP_PROTOCOL: "http/protobuf"}, clear=True)
def test_configurator_works_without_grpc(self):
# The k8s operator injects the distro from its own image into the application container, so the bundled
# grpcio may not be built for the application's Python version (e.g. cp312 wheels on Python 3.13) and
# grpc fails to import. The operator exports via http/protobuf so we must still configure the SDK.
import elasticotel.distro as distro_module

grpc_modules = {
name: None
for name in [*sys.modules, "grpc"]
if name.split(".")[0] == "grpc" or name.startswith("opentelemetry.exporter.otlp.proto.grpc")
}
self.addCleanup(importlib.reload, distro_module)
with (
mock.patch.dict(sys.modules, grpc_modules),
mock.patch(
"opentelemetry.sdk._configuration._initialize_components", wraps=_initialize_components
) as initialize_components_mock,
):
importlib.reload(distro_module)
distro_module.ElasticOpenTelemetryDistro()._configure()
distro_module.ElasticOpenTelemetryConfigurator()._configure()

self.assertIsInstance(trace.get_tracer_provider(), TracerProvider)
exporter_args_map = initialize_components_mock.call_args.kwargs["exporter_args_map"]
self.assertEqual(set(exporter_args_map), {HTTPOTLPLogExporter, HTTPOTLPMetricExporter, HTTPOTLPSpanExporter})
for exporter_args in exporter_args_map.values():
self.assertTrue(
exporter_args["headers"]["User-Agent"].startswith(distro_module.EDOT_HTTP_USER_AGENT_HEADER_VALUE)
)


class TestConfig(TestCase):
def tearDown(self):
Expand Down
Loading