Skip to content

Commit a4c169c

Browse files
committed
Merge branch 'main' of github.com:open-telemetry/opentelemetry-python into 4907
2 parents 9ca005d + 0f47f66 commit a4c169c

File tree

26 files changed

+4618
-781
lines changed

26 files changed

+4618
-781
lines changed

.github/workflows/stale.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,5 @@ jobs:
3333
This PR has been closed due to inactivity. Please reopen if you would
3434
like to continue working on it.
3535
exempt-pr-labels: "hold,WIP,blocked by spec,do not merge"
36+
# TODO: Revert back to default of 30 after we have cleared the backlog of stale PRs.
37+
operations-per-run: 500

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414

1515
- logs: add exception support to Logger emit and LogRecord attributes
1616
([#4907](https://github.com/open-telemetry/opentelemetry-python/issues/4907))
17+
- `opentelemetry-sdk`: Add file configuration support with YAML/JSON loading, environment variable substitution, and schema validation against the vendored OTel config JSON schema
18+
([#4898](https://github.com/open-telemetry/opentelemetry-python/pull/4898))
1719
- Fix intermittent CI failures in `getting-started` and `tracecontext` jobs caused by GitHub git CDN SHA propagation lag by installing contrib packages from the already-checked-out local copy instead of a second git clone
1820
([#4958](https://github.com/open-telemetry/opentelemetry-python/pull/4958))
1921
- `opentelemetry-sdk`: fix type annotations on `MetricReader` and related types
2022
([#4938](https://github.com/open-telemetry/opentelemetry-python/pull/4938/))
23+
- Implement log creation metric
24+
([#4935](https://github.com/open-telemetry/opentelemetry-python/pull/4935))
25+
- `opentelemetry-sdk`: upgrade vendored OTel configuration schema from v1.0.0-rc.3 to v1.0.0
26+
([#4965](https://github.com/open-telemetry/opentelemetry-python/pull/4965))
2127

2228
## Version 1.40.0/0.61b0 (2026-03-04)
2329

opentelemetry-api/tests/logs/test_proxy.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ def test_proxy_logger_forwards_exception_with_record(self):
8383
record = _logs.LogRecord()
8484
exception = ValueError("boom")
8585

86+
self.assertIsNotNone(logger._real_logger)
8687
logger.emit(record, exception=exception)
8788

8889
logger._real_logger.emit.assert_called_once_with(

opentelemetry-sdk/pyproject.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,12 @@ dependencies = [
3232
"typing-extensions >= 4.5.0",
3333
]
3434

35+
[project.optional-dependencies]
36+
file-configuration = [
37+
"pyyaml >= 6.0",
38+
"jsonschema >= 4.0",
39+
]
40+
3541
[project.entry-points.opentelemetry_environment_variables]
3642
sdk = "opentelemetry.sdk.environment_variables"
3743

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# SDK File Configuration
2+
3+
This package implements [OpenTelemetry file-based configuration](https://opentelemetry.io/docs/specs/otel/configuration).
4+
5+
## Files
6+
7+
- `schema.json` — vendored copy of the [OpenTelemetry configuration JSON schema](https://github.com/open-telemetry/opentelemetry-configuration)
8+
- `models.py` — Python dataclasses generated from `schema.json` by [datamodel-code-generator](https://github.com/koxudaxi/datamodel-code-generator)
9+
10+
## Updating the schema
11+
12+
1. Download the new schema from the [opentelemetry-configuration releases](https://github.com/open-telemetry/opentelemetry-configuration/releases):
13+
14+
```sh
15+
curl -o opentelemetry-sdk/src/opentelemetry/sdk/_configuration/schema.json \
16+
https://raw.githubusercontent.com/open-telemetry/opentelemetry-configuration/refs/tags/vX.Y.Z/opentelemetry_configuration.json
17+
```
18+
19+
2. Regenerate `models.py`:
20+
21+
```sh
22+
tox -e generate-config-from-jsonschema
23+
```
24+
25+
3. Update any version string references in tests and source:
26+
27+
```sh
28+
grep -r "OLD_VERSION" opentelemetry-sdk/
29+
```
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""OpenTelemetry SDK File Configuration.
16+
17+
This module provides support for configuring the OpenTelemetry SDK
18+
using declarative configuration files (YAML or JSON).
19+
20+
Example:
21+
>>> from opentelemetry.sdk._configuration.file import load_config_file
22+
>>> config = load_config_file("otel-config.yaml")
23+
>>> print(config.file_format)
24+
'1.0'
25+
"""
26+
27+
from opentelemetry.sdk._configuration.file._env_substitution import (
28+
EnvSubstitutionError,
29+
substitute_env_vars,
30+
)
31+
from opentelemetry.sdk._configuration.file._loader import (
32+
ConfigurationError,
33+
load_config_file,
34+
)
35+
36+
__all__ = [
37+
"load_config_file",
38+
"substitute_env_vars",
39+
"ConfigurationError",
40+
"EnvSubstitutionError",
41+
]
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Environment variable substitution for configuration files."""
16+
17+
import logging
18+
import os
19+
import re
20+
21+
_logger = logging.getLogger(__name__)
22+
23+
24+
class EnvSubstitutionError(Exception):
25+
"""Raised when environment variable substitution fails.
26+
27+
This occurs when a ${VAR} reference is found but the environment
28+
variable is not set and no default value is provided.
29+
"""
30+
31+
32+
def substitute_env_vars(text: str) -> str:
33+
"""Substitute environment variables in configuration text.
34+
35+
Supports the following syntax:
36+
- ${VAR}: Substitute with environment variable VAR. Raises error if not found.
37+
- ${VAR:-default}: Substitute with VAR if set, otherwise use default value.
38+
- $$: Escape sequence for literal $.
39+
40+
Args:
41+
text: Configuration text with potential ${VAR} placeholders.
42+
43+
Returns:
44+
Text with environment variables substituted.
45+
46+
Raises:
47+
EnvSubstitutionError: If a required environment variable is not found.
48+
49+
Examples:
50+
>>> os.environ['SERVICE_NAME'] = 'my-service'
51+
>>> substitute_env_vars('name: ${SERVICE_NAME}')
52+
'name: my-service'
53+
>>> substitute_env_vars('name: ${MISSING:-default}')
54+
'name: default'
55+
>>> substitute_env_vars('price: $$100')
56+
'price: $100'
57+
"""
58+
# Pattern matches $$ (escape sequence) or ${VAR_NAME} / ${VAR_NAME:-default_value}
59+
# Handling both in a single pass ensures $$ followed by ${VAR} works correctly
60+
pattern = r"\$\$|\$\{([A-Za-z_][A-Za-z0-9_]*)(:-([^}]*))?\}"
61+
62+
def replace_var(match) -> str:
63+
if match.group(1) is None:
64+
# Matched $$, return literal $
65+
return "$"
66+
67+
var_name = match.group(1)
68+
has_default = match.group(2) is not None
69+
default_value = match.group(3) if has_default else None
70+
71+
value = os.environ.get(var_name)
72+
73+
if value is None:
74+
if has_default:
75+
return default_value or ""
76+
_logger.error(
77+
"Environment variable '%s' not found and no default provided",
78+
var_name,
79+
)
80+
raise EnvSubstitutionError(
81+
f"Environment variable '{var_name}' not found and no default provided"
82+
)
83+
84+
return value
85+
86+
return re.sub(pattern, replace_var, text)

0 commit comments

Comments
 (0)