Skip to content
Draft
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
7 changes: 6 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ Added
``dash0_distribution`` (``telemetry.distro.*``), ``dash0_kubernetes``
(``k8s.pod.uid``) and ``dash0_service_name`` (upstream service detection —
``service.instance.id`` and ``OTEL_SERVICE_NAME`` — plus the distro's
service-name fallback).
service-name fallback). When declarative configuration is active
(``OTEL_CONFIG_FILE``), ``Dash0Configurator`` now adds these detectors to the
parsed configuration automatically, so those attributes are no longer dropped
(`#6
<https://github.com/dash0hq/opentelemetry-python-distribution/issues/6>`_);
attributes declared in the config file still take precedence.
- ``Dash0Distro`` and ``Dash0Configurator`` providing zero-code instrumentation,
pure-Python OTLP/HTTP export by default, an enable/disable gate, a Kubernetes
pod-UID resource detector, a service-name fallback, a ``telemetry.distro.name``
Expand Down
21 changes: 16 additions & 5 deletions packages/dash0-opentelemetry-distro/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ Entry points
(``DASH0_FLUSH_ON_SIGTERM_SIGINT=true``) — normal-exit flushing is already
handled by the SDK's ``atexit`` provider shutdown.

When **declarative configuration** is active (``OTEL_CONFIG_FILE``), the
configurator loads the config file itself and adds the distro's resource
detectors (see below) to the parsed configuration before handing it to the
SDK. Declarative configuration ignores
``OTEL_RESOURCE_ATTRIBUTES``/``OTEL_SERVICE_NAME``, so without this the
distro's detected resource attributes would silently disappear. Attributes
declared in the config file always take precedence over detected ones, and
detectors already listed in the file are not added twice.

Resource detectors (``opentelemetry_resource_detector``)
The detected resource attributes (see below) as standard SDK resource
detectors, one per concern so each can be used (or omitted) independently:
Expand All @@ -50,11 +59,13 @@ Resource detectors (``opentelemetry_resource_detector``)
``service.name`` fallback, so there is no need to also list the built-in
``service`` detector.

They can be referenced via ``OTEL_EXPERIMENTAL_RESOURCE_DETECTORS``, or
explicitly in a declarative config file under
``resource.detection/development.detectors``. (Note: on the pinned SDK
1.43, the upstream loader rejects the ``detection/development`` config-file
key — fixed upstream in 1.44.)
All three are added automatically in the declarative-configuration path;
they can also be referenced explicitly in a config file under
``resource.detection/development.detectors``, or via
``OTEL_EXPERIMENTAL_RESOURCE_DETECTORS``. (Note: on the pinned SDK 1.43,
the upstream loader rejects the ``detection/development`` config-file key —
fixed upstream in 1.44 — so the automatic injection is currently the only
way detectors run under declarative configuration.)

Resource detection
==================
Expand Down
5 changes: 5 additions & 0 deletions packages/dash0-opentelemetry-distro/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,11 @@ dash0_service_name = "dash0.opentelemetry.resource:Dash0ServiceNameResourceDetec
[dependency-groups]
dev = [
"pytest >= 7.0",
# Exercises the declarative-configuration (OTEL_CONFIG_FILE) code paths in
# the unit tests. On SDK 1.43 the declarative machinery ships inside the SDK
# behind this extra; whether it becomes a runtime dependency of the distro is
# tracked in issue #5.
"opentelemetry-sdk[file-configuration]",
]

[tool.hatch.version]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,4 @@
OTEL_SERVICE_NAME = "OTEL_SERVICE_NAME"
OTEL_RESOURCE_ATTRIBUTES = "OTEL_RESOURCE_ATTRIBUTES"
OTEL_SDK_DISABLED = "OTEL_SDK_DISABLED"
OTEL_CONFIG_FILE = "OTEL_CONFIG_FILE"
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
the Node.js distribution: an optional startup ("bootstrap") span and optional
graceful flushing on SIGTERM/SIGINT.

When ``OTEL_CONFIG_FILE`` is set, declarative configuration takes over and
ignores the resource environment variables the distro populated. In that case
this configurator loads the config file itself and adds the distro's resource
detectors to it, so ``telemetry.distro.*``, ``k8s.pod.uid`` and the fallback
``service.name`` survive (see :func:`_add_dash0_resource_detectors`).

Normal-exit flushing is already handled by the SDK: the tracer/meter/logger
providers register ``atexit`` shutdown hooks. The signal handling below covers
container termination, where ``atexit`` does not run by default.
Expand All @@ -22,11 +28,64 @@
from ._environment_variables import (
DASH0_BOOTSTRAP_SPAN,
DASH0_FLUSH_ON_SIGTERM_SIGINT,
OTEL_CONFIG_FILE,
)
from .settings import evaluate_gate, is_true

_logger = getLogger(__name__)

# Entry-point names under which the detectors in resource.py are registered in
# the ``opentelemetry_resource_detector`` group (see pyproject.toml).
_DASH0_RESOURCE_DETECTORS = (
"dash0_distribution",
"dash0_kubernetes",
"dash0_service_name",
)


def _load_declarative_machinery():
"""Return ``(load_config_file, configure_sdk, models)``.

SDK 1.44+ ships declarative configuration as the standalone
``opentelemetry-configuration`` package; on SDK 1.43 it lives inside the
SDK, behind the ``file-configuration`` extra. Raises ``ImportError`` when
neither is available.
"""
try:
from opentelemetry.configuration import (
configure_sdk,
load_config_file,
models,
)
except ImportError:
from opentelemetry.sdk._configuration import models
from opentelemetry.sdk._configuration._sdk import configure_sdk
from opentelemetry.sdk._configuration.file._loader import (
load_config_file,
)
return load_config_file, configure_sdk, models


def _add_dash0_resource_detectors(config, models):
"""Add the distro's resource detectors to a parsed declarative configuration.

Mutates ``config`` in place. Detectors already listed in the config file
are left alone; only the missing ones are appended.
"""
if config.resource is None:
config.resource = models.Resource()
if config.resource.detection_development is None:
config.resource.detection_development = models.ExperimentalResourceDetection()
detection = config.resource.detection_development
if detection.detectors is None:
detection.detectors = []
listed = set()
for detector in detection.detectors:
listed.update(getattr(detector, "additional_properties", {}))
missing = {name: None for name in _DASH0_RESOURCE_DETECTORS if name not in listed}
if missing:
detection.detectors.append(models.ExperimentalResourceDetector(**missing))


class Dash0Configurator(_OTelSDKConfigurator):
"""Configure the OpenTelemetry SDK for the Dash0 distribution."""
Expand All @@ -40,7 +99,8 @@ def _configure(self, **kwargs):
)
return

super()._configure(**kwargs)
if not self._configure_from_declarative_file():
super()._configure(**kwargs)

bootstrap_span_name = environ.get(DASH0_BOOTSTRAP_SPAN)
if bootstrap_span_name:
Expand Down Expand Up @@ -75,3 +135,41 @@ def _flush_and_reraise(signum, _frame):
_logger.debug(
"dash0: not on main thread, SIGTERM/SIGINT flush not installed"
)

def _configure_from_declarative_file(self):
"""Apply declarative configuration with the Dash0 detector added.

The base configurator handles ``OTEL_CONFIG_FILE`` too, but the
declarative resource builder ignores ``OTEL_RESOURCE_ATTRIBUTES`` and
``OTEL_SERVICE_NAME``, so the attributes the distro injected into the
environment would silently disappear (``telemetry.distro.*``,
``k8s.pod.uid``, the fallback ``service.name``). Loading the config
file here and adding the distro's resource detectors to the parsed
configuration keeps them, while attributes declared in the file still
win: detected attributes merge below explicit config attributes.

Returns ``True`` when declarative configuration was applied here, and
``False`` when the base configurator should run instead — either no
config file is set, or the declarative machinery is not importable (the
base class then reports its usual, more informative error).
"""
config_file = environ.get(OTEL_CONFIG_FILE)
if not config_file:
return False
try:
load_config_file, configure_sdk, models = _load_declarative_machinery()
except ImportError:
return False
config = load_config_file(config_file)
try:
_add_dash0_resource_detectors(config, models)
except Exception: # pylint: disable=broad-except
# The declarative config models are experimental; if they changed
# shape, still configure from the file rather than failing startup.
_logger.exception(
"dash0: could not add the Dash0 resource detectors to the "
"declarative configuration; telemetry.distro.* and k8s.pod.uid "
"resource attributes may be missing"
)
configure_sdk(config)
return True
128 changes: 128 additions & 0 deletions packages/dash0-opentelemetry-distro/tests/test_configurator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""Declarative-configuration (OTEL_CONFIG_FILE) handling of Dash0Configurator.

These tests need the declarative-configuration machinery (the
``opentelemetry-sdk[file-configuration]`` extra on SDK 1.43, a dev dependency,
or the standalone ``opentelemetry-configuration`` package on SDK 1.44+); they
are skipped when it is not installed.
"""

import pytest
from dash0.opentelemetry import resource as resource_module
from dash0.opentelemetry._environment_variables import (
DASH0_AUTOMATIC_SERVICE_NAME,
OTEL_CONFIG_FILE,
OTEL_RESOURCE_ATTRIBUTES,
OTEL_SERVICE_NAME,
)
from dash0.opentelemetry.configurator import (
Dash0Configurator,
_add_dash0_resource_detectors,
_load_declarative_machinery,
)

try:
load_config_file, _, models = _load_declarative_machinery()
except ImportError:
pytest.skip(
"declarative-configuration machinery not installed",
allow_module_level=True,
)

try:
import opentelemetry.configuration as _configure_sdk_module
from opentelemetry.configuration._resource import create_resource
except ImportError:
import opentelemetry.sdk._configuration._sdk as _configure_sdk_module
from opentelemetry.sdk._configuration._resource import create_resource

_MINIMAL_CONFIG = 'file_format: "1.0"\n'

_CONFIG_WITH_SERVICE_NAME = """\
file_format: "1.0"
resource:
attributes:
- name: service.name
value: from-file
"""


@pytest.fixture(autouse=True)
def _clean_env(monkeypatch):
for variable in (
OTEL_CONFIG_FILE,
OTEL_RESOURCE_ATTRIBUTES,
OTEL_SERVICE_NAME,
DASH0_AUTOMATIC_SERVICE_NAME,
):
monkeypatch.delenv(variable, raising=False)
monkeypatch.setattr(resource_module, "running_in_kubernetes", lambda: False)
monkeypatch.setattr(resource_module.sys, "argv", ["/opt/app/server.py"])


def _load(tmp_path, content):
config_file = tmp_path / "otel-config.yaml"
config_file.write_text(content, encoding="utf8")
return config_file


def test_detectors_injected_into_config_without_resource_section(tmp_path):
config = load_config_file(str(_load(tmp_path, _MINIMAL_CONFIG)))

_add_dash0_resource_detectors(config, models)
attributes = create_resource(config.resource).attributes

assert attributes["telemetry.distro.name"] == "dash0-python"
assert attributes["service.name"] == "server"
assert attributes["service.instance.id"]


def test_config_file_attributes_win_over_detected_ones(tmp_path):
config = load_config_file(str(_load(tmp_path, _CONFIG_WITH_SERVICE_NAME)))

_add_dash0_resource_detectors(config, models)
attributes = create_resource(config.resource).attributes

assert attributes["telemetry.distro.name"] == "dash0-python"
assert attributes["service.name"] == "from-file"


def test_detectors_already_listed_are_not_injected_twice(tmp_path):
config = load_config_file(str(_load(tmp_path, _MINIMAL_CONFIG)))
# Built programmatically rather than from YAML: on SDK 1.43 the loader
# rejects the spec's `detection/development` key (fixed in 1.44).
config.resource = models.Resource(
detection_development=models.ExperimentalResourceDetection(
detectors=[models.ExperimentalResourceDetector(dash0_kubernetes=None)]
)
)

_add_dash0_resource_detectors(config, models)

listed = [
name
for detector in config.resource.detection_development.detectors
for name in getattr(detector, "additional_properties", {})
]
assert listed.count("dash0_kubernetes") == 1
assert listed.count("dash0_distribution") == 1
assert listed.count("dash0_service_name") == 1


def test_configurator_skips_declarative_path_without_config_file():
assert Dash0Configurator()._configure_from_declarative_file() is False


def test_configurator_applies_declarative_config_with_detector(monkeypatch, tmp_path):
monkeypatch.setenv(OTEL_CONFIG_FILE, str(_load(tmp_path, _MINIMAL_CONFIG)))
applied = {}
monkeypatch.setattr(
_configure_sdk_module,
"configure_sdk",
lambda config: applied.update(config=config),
)

assert Dash0Configurator()._configure_from_declarative_file() is True

attributes = create_resource(applied["config"].resource).attributes
assert attributes["telemetry.distro.name"] == "dash0-python"
assert attributes["service.name"] == "server"
Loading
Loading