Skip to content

Commit 2aff7f6

Browse files
committed
Merge branch 'main' into weaver-live-check
2 parents 7e91f1b + 82128af commit 2aff7f6

File tree

15 files changed

+247
-158
lines changed

15 files changed

+247
-158
lines changed

AGENTS.md

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# OpenTelemetry Python
2+
3+
This file is here to steer AI assisted PRs towards being high quality and valuable contributions
4+
that do not create excessive maintainer burden.
5+
6+
Monorepo with the core OpenTelemetry Python API, SDK, and related packages.
7+
8+
## General Rules and Guidelines
9+
10+
The OpenTelemetry community has broader guidance on GenAI contributions at
11+
https://github.com/open-telemetry/community/blob/main/policies/genai.md — please read it before
12+
contributing.
13+
14+
The most important rule is not to post comments on issues or PRs that are AI-generated. Discussions
15+
on the OpenTelemetry repositories are for Users/Humans only.
16+
17+
Follow the PR scoping guidance in [CONTRIBUTING.md](CONTRIBUTING.md). Keep AI-assisted PRs tightly
18+
isolated to the requested change and never include unrelated cleanup or opportunistic improvements
19+
unless they are strictly necessary for correctness.
20+
21+
If you have been assigned an issue by the user or their prompt, please ensure that the
22+
implementation direction is agreed on with the maintainers first in the issue comments. If there are
23+
unknowns, discuss these on the issue before starting implementation. Do not forget that you cannot
24+
comment for users on issue threads on their behalf as it is against the rules of this project.
25+
26+
## Structure
27+
28+
- `opentelemetry-api/` - the OpenTelemetry API package
29+
- `opentelemetry-sdk/` - the OpenTelemetry SDK package
30+
- `opentelemetry-semantic-conventions/` - semantic conventions
31+
- `opentelemetry-proto/` / `opentelemetry-proto-json/` - protobuf definitions and generated code
32+
- `exporter/` - exporters (OTLP, Prometheus, Zipkin, etc.)
33+
- `propagator/` - context propagators (B3, Jaeger)
34+
- `shim/` - compatibility shims (OpenTracing, OpenCensus)
35+
36+
Each package lives under its own directory with a `pyproject.toml` and `tests/`.
37+
38+
## Commands
39+
40+
```sh
41+
# Install all packages and dev tools
42+
uv sync --frozen --all-packages
43+
44+
# Lint (runs ruff via pre-commit)
45+
uv run tox -e precommit
46+
47+
# Test a specific package
48+
uv run tox -e py312-test-opentelemetry-sdk
49+
50+
# Lint (pylint) a specific package
51+
uv run tox -e lint-opentelemetry-sdk
52+
53+
# Type check
54+
uv run tox -e typecheck
55+
```
56+
57+
## Guidelines
58+
59+
- Each package has its own `pyproject.toml` with version, dependencies, and entry points.
60+
- The monorepo uses `uv` workspaces.
61+
- `tox.ini` defines the test matrix - check it for available test environments.
62+
- Do not add `type: ignore` comments. If a type error arises, solve it properly or write a follow-up plan to address it in another PR.
63+
- Whenever applicable, all code changes should have tests that actually validate the changes.
64+
65+
## Commit formatting
66+
67+
We appreciate it if users disclose the use of AI tools when the significant part of a commit is
68+
taken from a tool without changes. When making a commit this should be disclosed through an
69+
`Assisted-by:` commit message trailer.
70+
71+
Examples:
72+
73+
```
74+
Assisted-by: ChatGPT 5.2
75+
Assisted-by: Claude Opus 4.6
76+
```

CHANGELOG.md

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

1313
## Unreleased
1414

15+
- `opentelemetry-sdk`: fix YAML structure injection via environment variable substitution in declarative file configuration; values containing newlines are now emitted as quoted YAML scalars per spec requirement
16+
([#5091](https://github.com/open-telemetry/opentelemetry-python/pull/5091))
1517
- `opentelemetry-sdk`: Add `create_logger_provider`/`configure_logger_provider` to declarative file configuration, enabling LoggerProvider instantiation from config files without reading env vars
1618
([#4990](https://github.com/open-telemetry/opentelemetry-python/pull/4990))
1719
- `opentelemetry-sdk`: Add `service` resource detector support to declarative file configuration via `detection_development.detectors[].service`
@@ -20,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2022
([#4907](https://github.com/open-telemetry/opentelemetry-python/issues/4907))
2123
- Drop Python 3.9 support
2224
([#5076](https://github.com/open-telemetry/opentelemetry-python/pull/5076))
25+
- `opentelemetry-semantic-conventions`: use `X | Y` union annotation
26+
([#5096](https://github.com/open-telemetry/opentelemetry-python/pull/5096))
2327
- Add WeaverLiveCheck test util
2428
([#5088](https://github.com/open-telemetry/opentelemetry-python/pull/5088))
2529

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
See [AGENTS.md](AGENTS.md).

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/file/_env_substitution.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,23 @@ def replace_var(match) -> str:
8181
f"Environment variable '{var_name}' not found and no default provided"
8282
)
8383

84+
# Per spec: "It MUST NOT be possible to inject YAML structures by
85+
# environment variables." Newlines are the primary injection vector —
86+
# a value like "legit\nmalicious_key: val" would create extra YAML
87+
# keys if substituted verbatim. Wrap such values in a YAML
88+
# double-quoted scalar so the newline is treated as literal text.
89+
# Simple values (no newlines) are returned as-is so that YAML type
90+
# coercion still applies per spec ("Node types MUST be interpreted
91+
# after environment variable substitution takes place").
92+
if "\n" in value or "\r" in value:
93+
escaped = (
94+
value.replace("\\", "\\\\")
95+
.replace('"', '\\"')
96+
.replace("\n", "\\n")
97+
.replace("\r", "\\r")
98+
.replace("\t", "\\t")
99+
)
100+
return f'"{escaped}"'
84101
return value
85102

86103
return re.sub(pattern, replace_var, text)

opentelemetry-sdk/tests/_configuration/file/test_env_substitution.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import unittest
1717
from unittest.mock import patch
1818

19+
import yaml
20+
1921
from opentelemetry.sdk._configuration.file import (
2022
EnvSubstitutionError,
2123
substitute_env_vars,
@@ -115,3 +117,45 @@ def test_only_dollar_signs(self):
115117
"""Test string with only escaped dollar signs."""
116118
result = substitute_env_vars("$$$$")
117119
self.assertEqual(result, "$$")
120+
121+
def test_newline_in_value_prevents_yaml_injection(self):
122+
"""Values containing newlines must not inject YAML structure.
123+
124+
Per spec: "It MUST NOT be possible to inject YAML structures by
125+
environment variables." A value like "legit\\nmalicious_key: val"
126+
must be emitted as a quoted scalar, not raw YAML.
127+
"""
128+
with patch.dict(
129+
os.environ,
130+
{"SERVICE_NAME": "legit-service\nmalicious_key: injected_value"},
131+
):
132+
result = substitute_env_vars(
133+
"file_format: '0.1'\nservice_name: ${SERVICE_NAME}"
134+
)
135+
parsed = yaml.safe_load(result)
136+
self.assertNotIn("malicious_key", parsed)
137+
self.assertIn("legit-service", parsed["service_name"])
138+
139+
def test_newline_in_value_preserved_as_literal(self):
140+
"""Newline within a value is preserved as a literal newline character."""
141+
with patch.dict(os.environ, {"MULTI": "line1\nline2"}):
142+
result = substitute_env_vars("key: ${MULTI}")
143+
parsed = yaml.safe_load(result)
144+
self.assertEqual(parsed["key"], "line1\nline2")
145+
146+
def test_carriage_return_in_value_is_escaped(self):
147+
"""Carriage return in value is escaped, not injected."""
148+
with patch.dict(os.environ, {"VAL": "text\r\nmore"}):
149+
result = substitute_env_vars("key: ${VAL}")
150+
parsed = yaml.safe_load(result)
151+
self.assertIsInstance(parsed["key"], str)
152+
153+
def test_type_coercion_preserved_for_simple_values(self):
154+
"""Simple values without newlines still undergo YAML type coercion per spec."""
155+
with patch.dict(os.environ, {"BOOL_VAL": "true", "INT_VAL": "42"}):
156+
bool_result = yaml.safe_load(
157+
substitute_env_vars("key: ${BOOL_VAL}")
158+
)
159+
int_result = yaml.safe_load(substitute_env_vars("key: ${INT_VAL}"))
160+
self.assertIs(bool_result["key"], True)
161+
self.assertEqual(int_result["key"], 42)

opentelemetry-sdk/tests/trace/test_span_processor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -461,7 +461,7 @@ def delayed_flush(_):
461461
for mock_processor in mocks:
462462
multi_processor.add_span_processor(mock_processor)
463463

464-
flushed = multi_processor.force_flush(timeout_millis=10)
464+
flushed = multi_processor.force_flush(timeout_millis=25)
465465
# let the thread executing the late_mock continue
466466
wait_event.set()
467467

opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics/container_metrics.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,8 @@
1313
# limitations under the License.
1414

1515

16-
from typing import (
17-
Callable,
18-
Final,
19-
Generator,
20-
Iterable,
21-
Optional,
22-
Sequence,
23-
Union,
24-
)
16+
from collections.abc import Callable, Generator, Iterable, Sequence
17+
from typing import Final
2518

2619
from opentelemetry.metrics import (
2720
CallbackOptions,
@@ -33,10 +26,10 @@
3326
)
3427

3528
# pylint: disable=invalid-name
36-
CallbackT = Union[
37-
Callable[[CallbackOptions], Iterable[Observation]],
38-
Generator[Iterable[Observation], CallbackOptions, None],
39-
]
29+
CallbackT = (
30+
Callable[[CallbackOptions], Iterable[Observation]]
31+
| Generator[Iterable[Observation], CallbackOptions, None]
32+
)
4033

4134
CONTAINER_CPU_TIME: Final = "container.cpu.time"
4235
"""
@@ -66,7 +59,7 @@ def create_container_cpu_time(meter: Meter) -> Counter:
6659

6760

6861
def create_container_cpu_usage(
69-
meter: Meter, callbacks: Optional[Sequence[CallbackT]]
62+
meter: Meter, callbacks: Sequence[CallbackT] | None
7063
) -> ObservableGauge:
7164
"""Container's CPU usage, measured in cpus. Range from 0 to the number of allocatable CPUs"""
7265
return meter.create_observable_gauge(
@@ -284,7 +277,7 @@ def create_container_network_io(meter: Meter) -> Counter:
284277

285278

286279
def create_container_uptime(
287-
meter: Meter, callbacks: Optional[Sequence[CallbackT]]
280+
meter: Meter, callbacks: Sequence[CallbackT] | None
288281
) -> ObservableGauge:
289282
"""The time the container has been running"""
290283
return meter.create_observable_gauge(

opentelemetry-semantic-conventions/src/opentelemetry/semconv/_incubating/metrics/cpu_metrics.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,8 @@
1313
# limitations under the License.
1414

1515

16-
from typing import (
17-
Callable,
18-
Final,
19-
Generator,
20-
Iterable,
21-
Optional,
22-
Sequence,
23-
Union,
24-
)
16+
from collections.abc import Callable, Generator, Iterable, Sequence
17+
from typing import Final
2518

2619
from opentelemetry.metrics import (
2720
CallbackOptions,
@@ -32,10 +25,10 @@
3225
)
3326

3427
# pylint: disable=invalid-name
35-
CallbackT = Union[
36-
Callable[[CallbackOptions], Iterable[Observation]],
37-
Generator[Iterable[Observation], CallbackOptions, None],
38-
]
28+
CallbackT = (
29+
Callable[[CallbackOptions], Iterable[Observation]]
30+
| Generator[Iterable[Observation], CallbackOptions, None]
31+
)
3932

4033
CPU_FREQUENCY: Final = "cpu.frequency"
4134
"""
@@ -44,7 +37,7 @@
4437

4538

4639
def create_cpu_frequency(
47-
meter: Meter, callbacks: Optional[Sequence[CallbackT]]
40+
meter: Meter, callbacks: Sequence[CallbackT] | None
4841
) -> ObservableGauge:
4942
"""Deprecated. Use `system.cpu.frequency` instead"""
5043
return meter.create_observable_gauge(
@@ -77,7 +70,7 @@ def create_cpu_time(meter: Meter) -> Counter:
7770

7871

7972
def create_cpu_utilization(
80-
meter: Meter, callbacks: Optional[Sequence[CallbackT]]
73+
meter: Meter, callbacks: Sequence[CallbackT] | None
8174
) -> ObservableGauge:
8275
"""Deprecated. Use `system.cpu.utilization` instead"""
8376
return meter.create_observable_gauge(

0 commit comments

Comments
 (0)