Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
43f3fe5
config: generate model code from json schema
codeboten Jan 15, 2026
72c3729
update tox command’s deps and allowlist
MikeGoldsmith Feb 4, 2026
a2dfa41
add use-union-operator to datamodel-codegen and regenerate models file
MikeGoldsmith Feb 4, 2026
659ab65
Merge branch 'main' of github.com:open-telemetry/opentelemetry-python…
MikeGoldsmith Feb 4, 2026
dd6a2cd
add changelog
MikeGoldsmith Feb 4, 2026
99e9570
disable union-operator and set target python to 3.10
MikeGoldsmith Feb 4, 2026
43f2c09
Merge pull request #13 from honeycombio/codeboten/generate-config-mod…
codeboten Feb 4, 2026
1a3bde8
ignore generated file for linting
MikeGoldsmith Feb 5, 2026
6936e54
fix TypeAlias import for python 3.9 in generated models file
MikeGoldsmith Feb 5, 2026
7949792
update uv.lock with new dev dependencies
MikeGoldsmith Feb 5, 2026
9f5f21e
Merge pull request #14 from honeycombio/codeboten/generate-config-mod…
codeboten Feb 5, 2026
81ac395
run precommit
codeboten Feb 5, 2026
2b25010
config: add yam/json file loading and env var substitution
MikeGoldsmith Feb 5, 2026
45bc753
Merge branch 'main' of github.com:open-telemetry/opentelemetry-python…
MikeGoldsmith Feb 11, 2026
65b2d16
Merge branch 'codeboten/generate-config-model-from-schema' of github.…
MikeGoldsmith Feb 11, 2026
801a425
Fix pyright config structure in pyproject.toml
MikeGoldsmith Feb 11, 2026
e8180f2
Merge branch 'codeboten/generate-config-model-from-schema' of github.…
MikeGoldsmith Feb 11, 2026
4c24e59
Fix typecheck and pylint errors
MikeGoldsmith Feb 11, 2026
dea74e9
Add types-PyYAML and file-configuration extra to typecheck
MikeGoldsmith Feb 11, 2026
df082ac
run precommit
MikeGoldsmith Feb 11, 2026
b22fc34
Merge branch 'main' into mike/otel-config/1-loading-validation
MikeGoldsmith Feb 11, 2026
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
2 changes: 1 addition & 1 deletion .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ignore=CVS,gen,proto

# Add files or directories matching the regex patterns to be excluded. The
# regex matches against base names, not paths.
ignore-patterns=
ignore-patterns=^models\.py$

# Python code to execute, usually for sys.path manipulation such as
# pygtk.require().
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- `opentelemetry-sdk`: Add file configuration support with YAML/JSON loading and environment variable substitution
- `opentelemetry-exporter-otlp-proto-grpc`: Fix re-initialization of gRPC channel on UNAVAILABLE error
([#4825](https://github.com/open-telemetry/opentelemetry-python/pull/4825))
- `opentelemetry-exporter-prometheus`: Fix duplicate HELP/TYPE declarations for metrics with different label sets
Expand All @@ -38,6 +39,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#4862](https://github.com/open-telemetry/opentelemetry-python/pull/4862))
- `opentelemetry-exporter-otlp-proto-http`: fix retry logic and error handling for connection failures in trace, metric, and log exporters
([#4709](https://github.com/open-telemetry/opentelemetry-python/pull/4709))
- `opentelemetry-sdk`: automatically generate configuration models using OTel config JSON schema
([#4879](https://github.com/open-telemetry/opentelemetry-python/pull/4879))

## Version 1.39.0/0.60b0 (2025-12-03)

Expand Down
5 changes: 5 additions & 0 deletions opentelemetry-sdk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ dependencies = [
"typing-extensions >= 4.5.0",
]

[project.optional-dependencies]
file-configuration = [
"pyyaml >= 6.0",
]

[project.entry-points.opentelemetry_environment_variables]
sdk = "opentelemetry.sdk.environment_variables"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""OpenTelemetry SDK File Configuration.

This module provides support for configuring the OpenTelemetry SDK
using declarative configuration files (YAML or JSON).

Example:
>>> from opentelemetry.sdk._configuration.file import load_config_file
>>> config = load_config_file("otel-config.yaml")
>>> print(config.file_format)
'1.0-rc.3'
"""

from opentelemetry.sdk._configuration.file._env_substitution import (
EnvSubstitutionError,
substitute_env_vars,
)
from opentelemetry.sdk._configuration.file._loader import (
ConfigurationError,
load_config_file,
)

__all__ = [
"load_config_file",
"substitute_env_vars",
"ConfigurationError",
"EnvSubstitutionError",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Environment variable substitution for configuration files."""

import logging
import os
import re

_logger = logging.getLogger(__name__)


class EnvSubstitutionError(Exception):
"""Raised when environment variable substitution fails.

This occurs when a ${VAR} reference is found but the environment
variable is not set and no default value is provided.
"""


def substitute_env_vars(text: str) -> str:
"""Substitute environment variables in configuration text.

Supports the following syntax:
- ${VAR}: Substitute with environment variable VAR. Raises error if not found.
- ${VAR:-default}: Substitute with VAR if set, otherwise use default value.
- $$: Escape sequence for literal $.

Args:
text: Configuration text with potential ${VAR} placeholders.

Returns:
Text with environment variables substituted.

Raises:
EnvSubstitutionError: If a required environment variable is not found.

Examples:
>>> os.environ['SERVICE_NAME'] = 'my-service'
>>> substitute_env_vars('name: ${SERVICE_NAME}')
'name: my-service'
>>> substitute_env_vars('name: ${MISSING:-default}')
'name: default'
>>> substitute_env_vars('price: $$100')
'price: $100'
"""
# First, handle escape sequences by temporarily replacing $$
# Use a placeholder that's unlikely to appear in config files
dollar_placeholder = "\x00DOLLAR\x00"
text = text.replace("$$", dollar_placeholder)

# Pattern matches: ${VAR_NAME} or ${VAR_NAME:-default_value}
# Variable names must start with letter or underscore, followed by alphanumerics or underscores
pattern = r"\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}"

def replace_var(match) -> str:
var_name = match.group(1)
has_default = match.group(2) is not None
default_value = match.group(3) if has_default else None

value = os.environ.get(var_name)

if value is None:
if has_default:
return default_value or ""
_logger.error(
"Environment variable '%s' not found and no default provided",
var_name,
)
raise EnvSubstitutionError(
f"Environment variable '{var_name}' not found and no default provided"
)

return value

# Perform substitution
text = re.sub(pattern, replace_var, text)

# Restore escaped dollar signs
text = text.replace(dollar_placeholder, "$")

return text
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Configuration file loading and parsing."""

import json
import logging
from pathlib import Path
from typing import Any

from opentelemetry.sdk._configuration.file._env_substitution import (
substitute_env_vars,
)
from opentelemetry.sdk._configuration.models import OpenTelemetryConfiguration

try:
import yaml
except ImportError as exc:
raise ImportError(
"File configuration requires pyyaml. "
"Install with: pip install opentelemetry-sdk[file-configuration]"
) from exc

_logger = logging.getLogger(__name__)


class ConfigurationError(Exception):
"""Raised when configuration file loading, parsing, or validation fails.

This includes errors from:
- File not found or inaccessible
- Invalid YAML/JSON syntax
- Schema validation failures
- Environment variable substitution errors
"""


def load_config_file(file_path: str) -> OpenTelemetryConfiguration:
"""Load and parse an OpenTelemetry configuration file.

Supports YAML and JSON formats. Performs environment variable substitution
before parsing.

Args:
file_path: Path to the configuration file (.yaml, .yml, or .json).

Returns:
Parsed OpenTelemetryConfiguration object.

Raises:
ConfigurationError: If file cannot be read, parsed, or validated.
EnvSubstitutionError: If required environment variable is missing.

Examples:
>>> config = load_config_file("otel-config.yaml")
>>> print(config.tracer_provider)
"""
path = Path(file_path)

if not path.exists():
_logger.error("Configuration file not found: %s", file_path)
raise ConfigurationError(f"Configuration file not found: {file_path}")

if not path.is_file():
_logger.error("Configuration path is not a file: %s", file_path)
raise ConfigurationError(
f"Configuration path is not a file: {file_path}"
)

try:
with open(path, encoding="utf-8") as config_file:
content = config_file.read()
except (OSError, IOError) as exc:
_logger.exception("Failed to read configuration file: %s", file_path)
raise ConfigurationError(
f"Failed to read configuration file: {file_path}"
) from exc

# Perform environment variable substitution
try:
content = substitute_env_vars(content)
except Exception as exc:
raise ConfigurationError(
f"Environment variable substitution failed: {exc}"
) from exc

# Parse based on file extension
suffix = path.suffix.lower()
try:
if suffix in (".yaml", ".yml"):
data = yaml.safe_load(content)
elif suffix == ".json":
data = json.loads(content)
else:
_logger.error("Unsupported file format: %s", suffix)
raise ConfigurationError(
f"Unsupported file format: {suffix}. Use .yaml, .yml, or .json"
)
except yaml.YAMLError as exc:
_logger.exception("Failed to parse YAML from %s", file_path)
raise ConfigurationError(f"Failed to parse YAML: {exc}") from exc
except json.JSONDecodeError as exc:
_logger.exception("Failed to parse JSON from %s", file_path)
raise ConfigurationError(f"Failed to parse JSON: {exc}") from exc

if data is None:
_logger.error("Configuration file is empty: %s", file_path)
raise ConfigurationError("Configuration file is empty")

if not isinstance(data, dict):
_logger.error(
"Configuration must be a mapping/object, got %s",
type(data).__name__,
)
raise ConfigurationError(
f"Configuration must be a mapping/object, got {type(data).__name__}"
)

# Convert to OpenTelemetryConfiguration model
try:
config = _dict_to_model(data)
except Exception as exc:
_logger.exception(
"Failed to validate configuration from %s", file_path
)
raise ConfigurationError(
f"Failed to validate configuration: {exc}"
) from exc

return config


def _dict_to_model(data: dict[str, Any]) -> OpenTelemetryConfiguration:
"""Convert dictionary to OpenTelemetryConfiguration model.

Uses the generated dataclass from models.py. This provides basic
validation through dataclass field types.

Args:
data: Parsed configuration dictionary.

Returns:
OpenTelemetryConfiguration instance.

Raises:
TypeError: If data doesn't match expected structure.
ValueError: If values are invalid.
"""
# The models.py file has dataclasses, so we need to recursively
# construct them from dictionaries. For now, use a simple approach
# that relies on dataclass construction.

# This is a simplified implementation. A more robust version would
# recursively handle nested dataclasses and discriminated unions.
# For PR 1, we're focusing on basic loading - validation can be
# enhanced in future PRs.

try:
# Attempt to construct the model
# This will work for simple cases but may need enhancement
# for complex nested structures
config = OpenTelemetryConfiguration(**data)
return config
except TypeError as exc:
# Provide more helpful error message
raise TypeError(
f"Configuration structure is invalid. "
f"Check that all required fields are present and correctly typed: {exc}"
) from exc
Loading
Loading