Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package python-opentelemetry-api for
openSUSE:Factory checked in at 2026-03-30 18:30:17
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-opentelemetry-api (Old)
and /work/SRC/openSUSE:Factory/.python-opentelemetry-api.new.1999 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "python-opentelemetry-api"
Mon Mar 30 18:30:17 2026 rev:22 rq:1343630 version:1.40.0
Changes:
--------
---
/work/SRC/openSUSE:Factory/python-opentelemetry-api/python-opentelemetry-api.changes
2026-01-06 17:41:10.961268384 +0100
+++
/work/SRC/openSUSE:Factory/.python-opentelemetry-api.new.1999/python-opentelemetry-api.changes
2026-03-30 18:31:11.610806423 +0200
@@ -1,0 +2,20 @@
+Mon Mar 23 22:08:22 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 1.40.0:
+ * bump semantic-conventions to v1.40.0
+ * Add stale PR GitHub Action
+ * Allow loading all resource detectors by setting
+ `OTEL_EXPERIMENTAL_RESOURCE_DETECTORS` to `*`
+ * Regenerate opentelemetry-proto code with v1.9.0 release
+ * Add python 3.14 support
+ * Silence events API warnings for internal users
+ * opentelemetry-sdk: make it possible to override the default
+ processors in the SDK configurator
+ * Prevent possible endless recursion from happening in
+ `SimpleLogRecordProcessor.on_emit`, (#4799) and (#4867).
+ * Implement span start/end metrics
+ * Add environment variable carriers to API
+ * Add experimental composable rule based sampler
+ * Make ConcurrentMultiSpanProcessor fork safe
+
+-------------------------------------------------------------------
Old:
----
opentelemetry_api-1.39.1.tar.gz
New:
----
opentelemetry_api-1.40.0.tar.gz
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ python-opentelemetry-api.spec ++++++
--- /var/tmp/diff_new_pack.PgKAM5/_old 2026-03-30 18:31:12.298835018 +0200
+++ /var/tmp/diff_new_pack.PgKAM5/_new 2026-03-30 18:31:12.302835184 +0200
@@ -1,7 +1,7 @@
#
# spec file for package python-opentelemetry-api
#
-# Copyright (c) 2025 SUSE LLC and contributors
+# Copyright (c) 2026 SUSE LLC and contributors
#
# All modifications and additions to the file contributed by third parties
# remain the property of their copyright owners, unless otherwise agreed
@@ -27,7 +27,7 @@
%{?sle15_python_module_pythons}
Name: python-opentelemetry-api%{?psuffix}
-Version: 1.39.1
+Version: 1.40.0
Release: 0
Summary: OpenTelemetry Python API
License: Apache-2.0
@@ -51,7 +51,7 @@
%endif
%if %{with test}
BuildRequires: %{python_module opentelemetry-api = %{version}}
-BuildRequires: %{python_module opentelemetry-test-utils = 0.60b0}
+BuildRequires: %{python_module opentelemetry-test-utils = 0.61b0}
BuildRequires: %{python_module pytest}
%endif
%python_subpackages
++++++ opentelemetry_api-1.39.1.tar.gz -> opentelemetry_api-1.40.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/opentelemetry_api-1.39.1/PKG-INFO
new/opentelemetry_api-1.40.0/PKG-INFO
--- old/opentelemetry_api-1.39.1/PKG-INFO 2020-02-02 01:00:00.000000000
+0100
+++ new/opentelemetry_api-1.40.0/PKG-INFO 2020-02-02 01:00:00.000000000
+0100
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: opentelemetry-api
-Version: 1.39.1
+Version: 1.40.0
Summary: OpenTelemetry Python API
Project-URL: Homepage,
https://github.com/open-telemetry/opentelemetry-python/tree/main/opentelemetry-api
Project-URL: Repository, https://github.com/open-telemetry/opentelemetry-python
@@ -17,6 +17,7 @@
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
+Classifier: Programming Language :: Python :: 3.14
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: importlib-metadata<8.8.0,>=6.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/opentelemetry_api-1.39.1/pyproject.toml
new/opentelemetry_api-1.40.0/pyproject.toml
--- old/opentelemetry_api-1.39.1/pyproject.toml 2020-02-02 01:00:00.000000000
+0100
+++ new/opentelemetry_api-1.40.0/pyproject.toml 2020-02-02 01:00:00.000000000
+0100
@@ -22,6 +22,7 @@
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
+ "Programming Language :: Python :: 3.14",
"Typing :: Typed",
]
dependencies = [
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/opentelemetry_api-1.39.1/src/opentelemetry/context/__init__.py
new/opentelemetry_api-1.40.0/src/opentelemetry/context/__init__.py
--- old/opentelemetry_api-1.39.1/src/opentelemetry/context/__init__.py
2020-02-02 01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/src/opentelemetry/context/__init__.py
2020-02-02 01:00:00.000000000 +0100
@@ -160,6 +160,7 @@
# FIXME This is a temporary location for the suppress instrumentation key.
# Once the decision around how to suppress instrumentation is made in the
# spec, this key should be moved accordingly.
+_ON_EMIT_RECURSION_COUNT_KEY = create_key("on_emit_recursion_count")
_SUPPRESS_INSTRUMENTATION_KEY = create_key("suppress_instrumentation")
_SUPPRESS_HTTP_INSTRUMENTATION_KEY = create_key(
"suppress_http_instrumentation"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/opentelemetry_api-1.39.1/src/opentelemetry/metrics/_internal/__init__.py
new/opentelemetry_api-1.40.0/src/opentelemetry/metrics/_internal/__init__.py
---
old/opentelemetry_api-1.39.1/src/opentelemetry/metrics/_internal/__init__.py
2020-02-02 01:00:00.000000000 +0100
+++
new/opentelemetry_api-1.40.0/src/opentelemetry/metrics/_internal/__init__.py
2020-02-02 01:00:00.000000000 +0100
@@ -118,7 +118,7 @@
Args:
name: The name of the instrumenting module.
- ``__name__`` may not be used as this can result in
+ ``__name__`` should be avoided as this can result in
different meter names if the meters are in different files.
It is better to use a fixed string that can be imported where
needed and used consistently as the name of the meter.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/opentelemetry_api-1.39.1/src/opentelemetry/metrics/_internal/instrument.py
new/opentelemetry_api-1.40.0/src/opentelemetry/metrics/_internal/instrument.py
---
old/opentelemetry_api-1.39.1/src/opentelemetry/metrics/_internal/instrument.py
2020-02-02 01:00:00.000000000 +0100
+++
new/opentelemetry_api-1.40.0/src/opentelemetry/metrics/_internal/instrument.py
2020-02-02 01:00:00.000000000 +0100
@@ -183,7 +183,14 @@
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
- pass
+ """Records an increment to the counter.
+
+ Args:
+ amount: The amount to increment the counter by. Must be
non-negative.
+ attributes: Optional set of attributes to associate with the
measurement.
+ context: Optional context to associate with the measurement. If not
+ provided, the current context is used.
+ """
class NoOpCounter(Counter):
@@ -234,7 +241,18 @@
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
- pass
+ """Records an increment or decrement to the counter.
+
+ Unlike `Counter`, the ``amount`` may be negative, allowing the
+ instrument to track values that go up and down (e.g. number of
+ active requests, queue depth).
+
+ Args:
+ amount: The amount to add to the counter. May be positive or
negative.
+ attributes: Optional set of attributes to associate with the
measurement.
+ context: Optional context to associate with the measurement. If not
+ provided, the current context is used.
+ """
class NoOpUpDownCounter(UpDownCounter):
@@ -376,7 +394,20 @@
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
- pass
+ """Records a measurement.
+
+ Used to report measurements that are likely to be statistically
+ meaningful, such as request durations, payload sizes, or any value
+ for which a distribution (e.g. percentiles) is useful.
+
+ Args:
+ amount: The measurement to record. Should be non-negative in most
+ cases; negative values are only meaningful when the histogram
+ is used to track signed deltas.
+ attributes: Optional set of attributes to associate with the
measurement.
+ context: Optional context to associate with the measurement. If not
+ provided, the current context is used.
+ """
class NoOpHistogram(Histogram):
@@ -486,7 +517,18 @@
attributes: Optional[Attributes] = None,
context: Optional[Context] = None,
) -> None:
- pass
+ """Records the current value of the gauge.
+
+ The gauge reports the last recorded value when observed. It is
+ intended for non-additive measurements where only the current
+ value matters (e.g. CPU utilisation percentage, room temperature).
+
+ Args:
+ amount: The current value to record.
+ attributes: Optional set of attributes to associate with the
measurement.
+ context: Optional context to associate with the measurement. If not
+ provided, the current context is used.
+ """
class NoOpGauge(Gauge):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/opentelemetry_api-1.39.1/src/opentelemetry/propagators/_envcarrier.py
new/opentelemetry_api-1.40.0/src/opentelemetry/propagators/_envcarrier.py
--- old/opentelemetry_api-1.39.1/src/opentelemetry/propagators/_envcarrier.py
1970-01-01 01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/src/opentelemetry/propagators/_envcarrier.py
2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,99 @@
+# 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.
+
+import os
+import typing
+from collections.abc import MutableMapping
+
+from opentelemetry.propagators.textmap import Getter, Setter
+
+
+class EnvironmentGetter(Getter[typing.Mapping[str, str]]):
+ """Getter implementation for extracting context and baggage from
environment variables.
+
+ EnvironmentGetter creates a case-insensitive lookup from the current
environment
+ variables at initialization time and provides simple data access without
validation.
+
+ Per the OpenTelemetry specification, environment variables are treated as
immutable
+ within a process. For environments where context-carrying environment
variables
+ change between logical requests (e.g., AWS Lambda's _X_AMZN_TRACE_ID),
create a
+ new EnvironmentGetter instance at the start of each request.
+
+ Example usage:
+ getter = EnvironmentGetter()
+ traceparent = getter.get({}, "traceparent")
+ """
+
+ def __init__(self):
+ # Create case-insensitive lookup from current environment
+ # Per spec: "creates an in-memory copy of the current environment
variables"
+ self.carrier: typing.Dict[str, str] = {
+ k.lower(): v for k, v in os.environ.items()
+ }
+
+ def get(
+ self, carrier: typing.Mapping[str, str], key: str
+ ) -> typing.Optional[typing.List[str]]:
+ """Get a value from the environment carrier for the given key.
+
+ Args:
+ carrier: Not used; maintained for interface compatibility with
Getter[CarrierT]
+ key: The key to look up (case-insensitive)
+
+ Returns:
+ A list with a single string value if the key exists, None
otherwise.
+ """
+ val = self.carrier.get(key.lower())
+ if val is None:
+ return None
+ if isinstance(val, typing.Iterable) and not isinstance(val, str):
+ return list(val)
+ return [val]
+
+ def keys(self, carrier: typing.Mapping[str, str]) -> typing.List[str]:
+ """Get all keys from the environment carrier.
+
+ Args:
+ carrier: Not used; maintained for interface compatibility with
Getter[CarrierT]
+
+ Returns:
+ List of all environment variable keys (lowercase).
+ """
+ return list(self.carrier.keys())
+
+
+class EnvironmentSetter(Setter[MutableMapping[str, str]]):
+ """Setter implementation for building environment variable dictionaries.
+
+ EnvironmentSetter builds a dictionary of environment variables that
+ can be passed to utilities like subprocess.run()
+
+ Example usage:
+ setter = EnvironmentSetter()
+ env_vars = {}
+ setter.set(env_vars, "traceparent", "00-trace-id-span-id-01")
+ subprocess.run(myCommand, env=env_vars)
+ """
+
+ def set(
+ self, carrier: MutableMapping[str, str], key: str, value: str
+ ) -> None:
+ """Set a value in the carrier dictionary for the given key.
+
+ Args:
+ carrier: Dictionary to store environment variables
+ key: The key to set (will be converted to uppercase)
+ value: The value to set
+ """
+ carrier[key.upper()] = value
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/opentelemetry_api-1.39.1/src/opentelemetry/trace/__init__.py
new/opentelemetry_api-1.40.0/src/opentelemetry/trace/__init__.py
--- old/opentelemetry_api-1.39.1/src/opentelemetry/trace/__init__.py
2020-02-02 01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/src/opentelemetry/trace/__init__.py
2020-02-02 01:00:00.000000000 +0100
@@ -208,7 +208,7 @@
Args:
instrumenting_module_name: The uniquely identifiable name for
instrumentation
scope, such as instrumentation library, package, module or
class name.
- ``__name__`` may not be used as this can result in
+ ``__name__`` should be avoided as this can result in
different tracer names if the tracers are in different files.
It is better to use a fixed string that can be imported where
needed and used consistently as the name of the tracer.
@@ -471,7 +471,21 @@
record_exception: bool = True,
set_status_on_exception: bool = True,
) -> "Span":
- return INVALID_SPAN
+ current_span = get_current_span(context)
+ if isinstance(current_span, NonRecordingSpan):
+ return current_span
+ parent_span_context = current_span.get_span_context()
+ if parent_span_context is not None and not isinstance(
+ parent_span_context, SpanContext
+ ):
+ logger.warning(
+ "Invalid span context for %s: %s",
+ current_span,
+ parent_span_context,
+ )
+ return INVALID_SPAN
+
+ return NonRecordingSpan(context=parent_span_context)
@_agnosticcontextmanager
def start_as_current_span(
@@ -486,7 +500,23 @@
set_status_on_exception: bool = True,
end_on_exit: bool = True,
) -> Iterator["Span"]:
- yield INVALID_SPAN
+ span = self.start_span(
+ name=name,
+ context=context,
+ kind=kind,
+ attributes=attributes,
+ links=links,
+ start_time=start_time,
+ record_exception=record_exception,
+ set_status_on_exception=set_status_on_exception,
+ )
+ with use_span(
+ span,
+ end_on_exit=end_on_exit,
+ record_exception=record_exception,
+ set_status_on_exception=set_status_on_exception,
+ ) as span:
+ yield span
@deprecated("You should use NoOpTracer. Deprecated since version 1.9.0.")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/opentelemetry_api-1.39.1/src/opentelemetry/util/_importlib_metadata.py
new/opentelemetry_api-1.40.0/src/opentelemetry/util/_importlib_metadata.py
--- old/opentelemetry_api-1.39.1/src/opentelemetry/util/_importlib_metadata.py
2020-02-02 01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/src/opentelemetry/util/_importlib_metadata.py
2020-02-02 01:00:00.000000000 +0100
@@ -36,7 +36,7 @@
return original_entry_points()
-def entry_points(**params):
+def entry_points(**params) -> EntryPoints:
"""Replacement for importlib_metadata.entry_points that caches getting all
the entry points.
That part can be very slow, and OTel uses this function many times."""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/opentelemetry_api-1.39.1/src/opentelemetry/version/__init__.py
new/opentelemetry_api-1.40.0/src/opentelemetry/version/__init__.py
--- old/opentelemetry_api-1.39.1/src/opentelemetry/version/__init__.py
2020-02-02 01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/src/opentelemetry/version/__init__.py
2020-02-02 01:00:00.000000000 +0100
@@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-__version__ = "1.39.1"
+__version__ = "1.40.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/opentelemetry_api-1.39.1/tests/propagators/test__envcarrier.py
new/opentelemetry_api-1.40.0/tests/propagators/test__envcarrier.py
--- old/opentelemetry_api-1.39.1/tests/propagators/test__envcarrier.py
1970-01-01 01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/tests/propagators/test__envcarrier.py
2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,551 @@
+# 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.
+
+# type: ignore
+
+import os
+import unittest
+from unittest.mock import Mock, patch
+
+from opentelemetry import trace
+from opentelemetry.baggage import get_all, set_baggage
+from opentelemetry.baggage.propagation import W3CBaggagePropagator
+from opentelemetry.context import Context, get_current
+from opentelemetry.propagators._envcarrier import (
+ EnvironmentGetter,
+ EnvironmentSetter,
+)
+from opentelemetry.trace.propagation.tracecontext import (
+ TraceContextTextMapPropagator,
+)
+
+
+class TestEnvironmentGetter(unittest.TestCase):
+ """Unit tests for EnvironmentGetter."""
+
+ def test_get_existing_env_var(self):
+ """Test retrieving an existing environment variable."""
+ with patch.dict(os.environ, {"TEST_KEY": "test_value"}):
+ getter = EnvironmentGetter()
+ result = getter.get({}, "test_key")
+ self.assertEqual(result, ["test_value"])
+
+ def test_get_case_insensitive(self):
+ """Test case insensitive lookup for environment variables."""
+ with patch.dict(os.environ, {"TEST_KEY": "test_value"}):
+ getter = EnvironmentGetter()
+ self.assertEqual(getter.get({}, "test_key"), ["test_value"])
+ self.assertEqual(getter.get({}, "TEST_KEY"), ["test_value"])
+ self.assertEqual(getter.get({}, "Test_Key"), ["test_value"])
+
+ def test_get_nonexistent_env_var(self):
+ """Test retrieving a non-existent environment variable."""
+ with patch.dict(os.environ, {}, clear=True):
+ getter = EnvironmentGetter()
+ result = getter.get({}, "nonexistent_key")
+ self.assertIsNone(result)
+
+ def test_get_empty_value(self):
+ """Test retrieving an environment variable with empty value."""
+ with patch.dict(os.environ, {"EMPTY_KEY": ""}):
+ getter = EnvironmentGetter()
+ result = getter.get({}, "empty_key")
+ self.assertEqual(result, [""])
+
+ def test_get_with_special_characters(self):
+ """Test environment variables with special characters."""
+ with patch.dict(
+ os.environ, {"TEST_KEY": "value with spaces and !@#$%"}
+ ):
+ getter = EnvironmentGetter()
+ result = getter.get({}, "test_key")
+ self.assertEqual(result, ["value with spaces and !@#$%"])
+
+ def test_keys(self):
+ """Test getting all environment variable keys."""
+ test_env = {"KEY1": "value1", "KEY2": "value2", "key3": "value3"}
+ with patch.dict(os.environ, test_env, clear=True):
+ getter = EnvironmentGetter()
+ keys = getter.keys({})
+ expected_keys = {"key1", "key2", "key3"}
+ self.assertEqual(set(keys), expected_keys)
+
+ def test_keys_empty_environment(self):
+ """Test getting keys when environment is empty."""
+ with patch.dict(os.environ, {}, clear=True):
+ getter = EnvironmentGetter()
+ keys = getter.keys({})
+ self.assertEqual(keys, [])
+
+ def test_uses_snapshot_not_carrier_parameter(self):
+ """Test that getter uses internal snapshot, not carrier parameter.
+
+ The carrier parameter exists for interface compatibility with
+ Getter[CarrierT], but EnvironmentGetter reads from os.environ at
+ initialization, creating an immutable snapshot.
+ """
+ with patch.dict(os.environ, {"TEST_KEY": "test_value"}):
+ getter = EnvironmentGetter()
+ # Both return same value from snapshot, carrier is ignored
+ result1 = getter.get({}, "test_key")
+ result2 = getter.get({"test_key": "different"}, "test_key")
+ self.assertEqual(result1, ["test_value"])
+ self.assertEqual(result2, ["test_value"])
+
+ def test_snapshot_immutability(self):
+ """Test that getter snapshot doesn't see changes after
initialization."""
+ with patch.dict(os.environ, {}, clear=True):
+ getter = EnvironmentGetter()
+ self.assertIsNone(getter.get({}, "test_key"))
+
+ # Add environment variable after initialization
+ os.environ["TEST_KEY"] = "new_value"
+
+ # Getter should still not see the new value
+ self.assertIsNone(getter.get({}, "test_key"))
+
+
+class TestEnvironmentSetter(unittest.TestCase):
+ """Unit tests for EnvironmentSetter."""
+
+ def test_set_basic(self):
+ """Test setting a value in carrier dictionary."""
+ setter = EnvironmentSetter()
+ carrier = {}
+ setter.set(carrier, "test_key", "test_value")
+ self.assertEqual(carrier, {"TEST_KEY": "test_value"})
+
+ def test_set_multiple_values(self):
+ """Test setting multiple values in the same carrier."""
+ setter = EnvironmentSetter()
+ carrier = {}
+ setter.set(carrier, "key1", "value1")
+ setter.set(carrier, "key2", "value2")
+ setter.set(carrier, "key3", "value3")
+ expected = {"KEY1": "value1", "KEY2": "value2", "KEY3": "value3"}
+ self.assertEqual(carrier, expected)
+
+ def test_set_overwrites_existing(self):
+ """Test that setting a key overwrites existing value."""
+ setter = EnvironmentSetter()
+ carrier = {"TEST_KEY": "old_value"}
+ setter.set(carrier, "test_key", "new_value")
+ self.assertEqual(carrier, {"TEST_KEY": "new_value"})
+
+ def test_set_uppercases_keys(self):
+ """Test that keys are normalized to uppercase."""
+ setter = EnvironmentSetter()
+ carrier = {}
+ setter.set(carrier, "lowercase_key", "value1")
+ setter.set(carrier, "UPPERCASE_KEY", "value2")
+ setter.set(carrier, "MiXeD_cAsE_kEy", "value3")
+ expected = {
+ "LOWERCASE_KEY": "value1",
+ "UPPERCASE_KEY": "value2",
+ "MIXED_CASE_KEY": "value3",
+ }
+ self.assertEqual(carrier, expected)
+
+ def test_set_special_characters_in_value(self):
+ """Test setting values with special characters."""
+ setter = EnvironmentSetter()
+ carrier = {}
+ setter.set(carrier, "test_key", "value with spaces and !@#$%^&*()")
+ self.assertEqual(
+ carrier, {"TEST_KEY": "value with spaces and !@#$%^&*()"}
+ )
+
+ def test_set_empty_value(self):
+ """Test setting an empty value."""
+ setter = EnvironmentSetter()
+ carrier = {}
+ setter.set(carrier, "empty_key", "")
+ self.assertEqual(carrier, {"EMPTY_KEY": ""})
+
+ def test_does_not_modify_os_environ(self):
+ """Test that setter does not modify os.environ."""
+ setter = EnvironmentSetter()
+ carrier = {}
+ original_environ = dict(os.environ)
+ setter.set(carrier, "test_key", "test_value")
+ self.assertEqual(dict(os.environ), original_environ)
+ self.assertEqual(carrier, {"TEST_KEY": "test_value"})
+
+
+class TestEnvironmentCarrierWithTraceContext(unittest.TestCase):
+ """Integration tests with W3C TraceContext propagator."""
+
+ TRACE_ID = 0x4BF92F3577B34DA6A3CE929D0E0E4736
+ SPAN_ID = 0x00F067AA0BA902B7
+
+ def setUp(self):
+ self.propagator = TraceContextTextMapPropagator()
+
+ def _extract_with_env(self, env_vars):
+ """Helper: Extract context from environment variables."""
+ with patch.dict(os.environ, env_vars, clear=True):
+ getter = EnvironmentGetter()
+ return self.propagator.extract({}, getter=getter)
+
+ def _inject_to_env(self, context):
+ """Helper: Inject context into environment dict."""
+ setter = EnvironmentSetter()
+ env_dict = {}
+ self.propagator.inject(env_dict, context=context, setter=setter)
+ return env_dict
+
+ def test_extract_valid_traceparent(self):
+ """Test extracting valid traceparent from environment."""
+ traceparent = f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01"
+ ctx = self._extract_with_env({"TRACEPARENT": traceparent})
+
+ span_context = trace.get_current_span(ctx).get_span_context()
+ self.assertEqual(span_context.trace_id, self.TRACE_ID)
+ self.assertEqual(span_context.span_id, self.SPAN_ID)
+ self.assertTrue(span_context.trace_flags.sampled)
+ self.assertTrue(span_context.is_remote)
+
+ def test_extract_traceparent_not_sampled(self):
+ """Test extracting traceparent with sampled=false."""
+ traceparent = f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-00"
+ ctx = self._extract_with_env({"TRACEPARENT": traceparent})
+
+ span_context = trace.get_current_span(ctx).get_span_context()
+ self.assertFalse(span_context.trace_flags.sampled)
+
+ def test_extract_with_tracestate(self):
+ """Test extracting traceparent with tracestate."""
+ traceparent = f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01"
+ tracestate = "vendor1=value1,vendor2=value2"
+
+ ctx = self._extract_with_env(
+ {"TRACEPARENT": traceparent, "TRACESTATE": tracestate}
+ )
+
+ span_context = trace.get_current_span(ctx).get_span_context()
+ self.assertEqual(span_context.trace_state.get("vendor1"), "value1")
+ self.assertEqual(span_context.trace_state.get("vendor2"), "value2")
+
+ def test_extract_invalid_traceparent(self):
+ """Test that invalid traceparent formats are handled gracefully.
+
+ Per W3C Trace Context spec, invalid traceparent should be ignored.
+ """
+ invalid_traceparents = [
+ "invalid-format",
+ "00-00000000000000000000000000000000-1234567890123456-00", # zero
trace_id
+ "00-12345678901234567890123456789012-0000000000000000-00", # zero
span_id
+ "00-12345678901234567890123456789012-1234567890123456-00-extra",
# extra data
+ ]
+
+ for invalid_tp in invalid_traceparents:
+ with self.subTest(traceparent=invalid_tp):
+ ctx = self._extract_with_env({"TRACEPARENT": invalid_tp})
+ span = trace.get_current_span(ctx)
+ self.assertEqual(
+ span.get_span_context(), trace.INVALID_SPAN_CONTEXT
+ )
+
+ def test_extract_missing_traceparent(self):
+ """Test extraction with missing TRACEPARENT."""
+ ctx = self._extract_with_env({})
+ span = trace.get_current_span(ctx)
+ self.assertIsInstance(span.get_span_context(), trace.SpanContext)
+
+ def test_extract_preserves_context_on_invalid_traceparent(self):
+ """Test that invalid traceparent preserves original context."""
+ orig_ctx = Context({"my_key": "my_value"})
+
+ with patch.dict(os.environ, {"TRACEPARENT": "invalid"}, clear=True):
+ getter = EnvironmentGetter()
+ ctx = self.propagator.extract({}, context=orig_ctx, getter=getter)
+
+ self.assertDictEqual(ctx, orig_ctx)
+
+ def test_inject_valid_span_context(self):
+ """Test injecting valid span context to environment dict."""
+ span_context = trace.SpanContext(
+ trace_id=self.TRACE_ID,
+ span_id=self.SPAN_ID,
+ is_remote=False,
+ trace_flags=trace.TraceFlags(0x01),
+ )
+ ctx = trace.set_span_in_context(trace.NonRecordingSpan(span_context))
+
+ env_dict = self._inject_to_env(ctx)
+
+ expected_traceparent = (
+ f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01"
+ )
+ self.assertEqual(env_dict["TRACEPARENT"], expected_traceparent)
+
+ def test_inject_does_not_include_empty_tracestate(self):
+ """Test that empty tracestate is not injected.
+
+ Per W3C spec: vendors SHOULD avoid sending empty tracestate.
+ """
+ span_context = trace.SpanContext(
+ trace_id=self.TRACE_ID,
+ span_id=self.SPAN_ID,
+ is_remote=False,
+ )
+ ctx = trace.set_span_in_context(trace.NonRecordingSpan(span_context))
+
+ env_dict = self._inject_to_env(ctx)
+
+ self.assertIn("TRACEPARENT", env_dict)
+ self.assertNotIn("TRACESTATE", env_dict)
+
+ def test_inject_invalid_context(self):
+ """Test that invalid context is not propagated."""
+ ctx = trace.set_span_in_context(trace.INVALID_SPAN)
+ env_dict = self._inject_to_env(ctx)
+ self.assertNotIn("TRACEPARENT", env_dict)
+
+ def test_roundtrip_preserves_traceparent(self):
+ """Test that traceparent survives extract->inject->extract cycle."""
+ original_traceparent = (
+ f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01"
+ )
+
+ # Extract from environment
+ ctx1 = self._extract_with_env({"TRACEPARENT": original_traceparent})
+
+ # Inject to new environment dict
+ env_dict = self._inject_to_env(ctx1)
+ self.assertEqual(env_dict["TRACEPARENT"], original_traceparent)
+
+ # Extract again from injected dict
+ ctx2 = self._extract_with_env(env_dict)
+
+ # Verify both contexts have same span context
+ span1 = trace.get_current_span(ctx1).get_span_context()
+ span2 = trace.get_current_span(ctx2).get_span_context()
+ self.assertEqual(span1.trace_id, span2.trace_id)
+ self.assertEqual(span1.span_id, span2.span_id)
+ self.assertEqual(span1.trace_flags, span2.trace_flags)
+
+ def test_roundtrip_preserves_tracestate(self):
+ """Test that tracestate survives roundtrip."""
+ env_vars = {
+ "TRACEPARENT": f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01",
+ "TRACESTATE": "vendor1=value1,vendor2=value2",
+ }
+
+ ctx1 = self._extract_with_env(env_vars)
+ env_dict = self._inject_to_env(ctx1)
+
+ self.assertIn("TRACESTATE", env_dict)
+ self.assertIn("vendor1=value1", env_dict["TRACESTATE"])
+ self.assertIn("vendor2=value2", env_dict["TRACESTATE"])
+
+ def test_case_handling(self):
+ """Test case handling for W3C headers."""
+ test_env = {
+ "TRACEPARENT": f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01",
+ "TRACESTATE": "vendor=value",
+ }
+
+ with patch.dict(os.environ, test_env, clear=True):
+ getter = EnvironmentGetter()
+ # Propagator uses lowercase keys
+ self.assertIsNotNone(getter.get({}, "traceparent"))
+ self.assertIsNotNone(getter.get({}, "tracestate"))
+
+ @patch("opentelemetry.trace.INVALID_SPAN_CONTEXT")
+ @patch("opentelemetry.trace.get_current_span")
+ def test_fields(self, mock_get_current_span, mock_invalid_span_context):
+ """Test that propagator.fields matches injected keys."""
+ # pylint: disable=import-outside-toplevel
+ from opentelemetry.trace.span import TraceState # noqa: PLC0415
+
+ mock_get_current_span.configure_mock(
+ return_value=Mock(
+ **{
+ "get_span_context.return_value": Mock(
+ **{
+ "trace_id": 1,
+ "span_id": 2,
+ "trace_flags": 3,
+ "trace_state": TraceState([("a", "b")]),
+ }
+ )
+ }
+ )
+ )
+
+ mock_setter = Mock()
+ self.propagator.inject({}, setter=mock_setter)
+
+ inject_fields = set()
+ for mock_call in mock_setter.mock_calls:
+ inject_fields.add(mock_call[1][1])
+
+ self.assertEqual(inject_fields, self.propagator.fields)
+
+
+class TestEnvironmentCarrierWithBaggage(unittest.TestCase):
+ """Integration tests with W3C Baggage propagator."""
+
+ def setUp(self):
+ self.propagator = W3CBaggagePropagator()
+
+ def _extract_with_env(self, env_vars):
+ """Helper: Extract baggage from environment variables."""
+ with patch.dict(os.environ, env_vars, clear=True):
+ getter = EnvironmentGetter()
+ return self.propagator.extract({}, getter=getter)
+
+ def _inject_to_env(self, context):
+ """Helper: Inject baggage into environment dict."""
+ setter = EnvironmentSetter()
+ env_dict = {}
+ self.propagator.inject(env_dict, context=context, setter=setter)
+ return env_dict
+
+ def test_extract_baggage(self):
+ """Test extracting baggage from BAGGAGE environment variable."""
+ ctx = self._extract_with_env(
+ {"BAGGAGE": "key1=value1,key2=value2,key3=value3"}
+ )
+
+ baggage = get_all(ctx)
+ self.assertEqual(
+ baggage, {"key1": "value1", "key2": "value2", "key3": "value3"}
+ )
+
+ def test_extract_empty_baggage(self):
+ """Test extracting empty baggage."""
+ ctx = self._extract_with_env({"BAGGAGE": ""})
+ baggage = get_all(ctx)
+ self.assertEqual(baggage, {})
+
+ def test_extract_missing_baggage(self):
+ """Test extraction with missing BAGGAGE."""
+ ctx = self._extract_with_env({})
+ baggage = get_all(ctx)
+ self.assertEqual(baggage, {})
+
+ def test_inject_baggage(self):
+ """Test injecting baggage into environment dict."""
+ ctx = get_current()
+ ctx = set_baggage("deployment", "production", context=ctx)
+ ctx = set_baggage("service", "api-gateway", context=ctx)
+
+ env_dict = self._inject_to_env(ctx)
+
+ self.assertIn("BAGGAGE", env_dict)
+ baggage_value = env_dict["BAGGAGE"]
+ self.assertIn("deployment=production", baggage_value)
+ self.assertIn("service=api-gateway", baggage_value)
+
+ def test_inject_no_baggage(self):
+ """Test that empty baggage is not injected."""
+ ctx = get_current()
+ env_dict = self._inject_to_env(ctx)
+ self.assertNotIn("BAGGAGE", env_dict)
+
+ def test_roundtrip_baggage(self):
+ """Test baggage roundtrip."""
+ ctx1 = get_current()
+ ctx1 = set_baggage("user_id", "12345", context=ctx1)
+ ctx1 = set_baggage("session_id", "abc-def", context=ctx1)
+
+ env_dict = self._inject_to_env(ctx1)
+ ctx2 = self._extract_with_env(env_dict)
+
+ baggage1 = get_all(ctx1)
+ baggage2 = get_all(ctx2)
+ self.assertEqual(baggage1, baggage2)
+
+ @patch("opentelemetry.baggage.propagation.get_all")
+ @patch("opentelemetry.baggage.propagation._format_baggage")
+ def test_fields(self, mock_format_baggage, mock_get_all):
+ """Test that propagator.fields matches injected keys."""
+ mock_setter = Mock()
+ self.propagator.inject({}, setter=mock_setter)
+
+ inject_fields = set()
+ for mock_call in mock_setter.mock_calls:
+ inject_fields.add(mock_call[1][1])
+
+ self.assertEqual(inject_fields, self.propagator.fields)
+
+
+class TestEnvironmentCarrierWithCompositePropagator(unittest.TestCase):
+ """Integration tests with multiple propagators."""
+
+ TRACE_ID = 0x4BF92F3577B34DA6A3CE929D0E0E4736
+ SPAN_ID = 0x00F067AA0BA902B7
+
+ def setUp(self):
+ # pylint: disable=import-outside-toplevel
+ from opentelemetry.propagators.composite import ( # noqa: PLC0415
+ CompositePropagator,
+ )
+
+ self.propagator = CompositePropagator(
+ [TraceContextTextMapPropagator(), W3CBaggagePropagator()]
+ )
+
+ def test_extract_all_w3c_headers(self):
+ """Test extracting both traceparent and baggage."""
+ env_vars = {
+ "TRACEPARENT": f"00-{self.TRACE_ID:032x}-{self.SPAN_ID:016x}-01",
+ "TRACESTATE": "vendor=value",
+ "BAGGAGE": "user_id=12345,session_id=abc-def",
+ }
+
+ with patch.dict(os.environ, env_vars, clear=True):
+ getter = EnvironmentGetter()
+ ctx = self.propagator.extract({}, getter=getter)
+
+ # Verify traceparent was extracted
+ span_context = trace.get_current_span(ctx).get_span_context()
+ self.assertEqual(span_context.trace_id, self.TRACE_ID)
+ self.assertEqual(span_context.span_id, self.SPAN_ID)
+
+ # Verify baggage was extracted
+ baggage = get_all(ctx)
+ self.assertEqual(baggage["user_id"], "12345")
+ self.assertEqual(baggage["session_id"], "abc-def")
+
+ def test_inject_all_w3c_headers(self):
+ """Test injecting both traceparent and baggage."""
+ span_context = trace.SpanContext(
+ trace_id=self.TRACE_ID,
+ span_id=self.SPAN_ID,
+ is_remote=False,
+ trace_flags=trace.TraceFlags(0x01),
+ )
+ ctx = trace.set_span_in_context(trace.NonRecordingSpan(span_context))
+ ctx = set_baggage("deployment", "production", context=ctx)
+
+ setter = EnvironmentSetter()
+ env_dict = {}
+ self.propagator.inject(env_dict, context=ctx, setter=setter)
+
+ # Verify both were injected with uppercase keys
+ self.assertIn("TRACEPARENT", env_dict)
+ self.assertIn("BAGGAGE", env_dict)
+ self.assertIn("deployment=production", env_dict["BAGGAGE"])
+
+ def test_empty_environment(self):
+ """Test behavior with completely empty environment."""
+ with patch.dict(os.environ, {}, clear=True):
+ getter = EnvironmentGetter()
+ ctx = self.propagator.extract({}, getter=getter)
+
+ # Should not crash, return valid context
+ self.assertIsInstance(ctx, Context)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/opentelemetry_api-1.39.1/tests/test_implementation.py
new/opentelemetry_api-1.40.0/tests/test_implementation.py
--- old/opentelemetry_api-1.39.1/tests/test_implementation.py 2020-02-02
01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/tests/test_implementation.py 2020-02-02
01:00:00.000000000 +0100
@@ -17,6 +17,63 @@
from opentelemetry import trace
+class RecordingSpan(trace.Span):
+ def __init__(self, context: trace.SpanContext) -> None:
+ self._context = context
+
+ def get_span_context(self) -> trace.SpanContext:
+ return self._context
+
+ def is_recording(self) -> bool:
+ return True
+
+ def end(self, end_time=None) -> None:
+ pass
+
+ def set_attributes(self, attributes) -> None:
+ pass
+
+ def set_attribute(self, key, value) -> None:
+ pass
+
+ def add_event(
+ self,
+ name: str,
+ attributes=None,
+ timestamp=None,
+ ) -> None:
+ pass
+
+ def add_link(
+ self,
+ context,
+ attributes=None,
+ ) -> None:
+ pass
+
+ def update_name(self, name) -> None:
+ pass
+
+ def set_status(
+ self,
+ status,
+ description=None,
+ ) -> None:
+ pass
+
+ def record_exception(
+ self,
+ exception,
+ attributes=None,
+ timestamp=None,
+ escaped=False,
+ ) -> None:
+ pass
+
+ def __repr__(self) -> str:
+ return f"RecordingSpan({self._context!r})"
+
+
class TestAPIOnlyImplementation(unittest.TestCase):
"""
This test is in place to ensure the API is returning values that
@@ -36,18 +93,45 @@
tracer_provider = trace.NoOpTracerProvider()
tracer = tracer_provider.get_tracer(__name__)
with tracer.start_span("test") as span:
- self.assertEqual(
- span.get_span_context(), trace.INVALID_SPAN_CONTEXT
- )
- self.assertEqual(span, trace.INVALID_SPAN)
+ self.assertFalse(span.get_span_context().is_valid)
self.assertIs(span.is_recording(), False)
with tracer.start_span("test2") as span2:
- self.assertEqual(
- span2.get_span_context(), trace.INVALID_SPAN_CONTEXT
- )
- self.assertEqual(span2, trace.INVALID_SPAN)
+ self.assertFalse(span2.get_span_context().is_valid)
self.assertIs(span2.is_recording(), False)
+ def test_default_tracer_context_propagation_recording_span(self):
+ tracer_provider = trace.NoOpTracerProvider()
+ tracer = tracer_provider.get_tracer(__name__)
+ span_context = trace.SpanContext(
+ 2604504634922341076776623263868986797,
+ 5213367945872657620,
+ False,
+ trace.TraceFlags(0x01),
+ )
+ ctx = trace.set_span_in_context(RecordingSpan(context=span_context))
+ with tracer.start_span("test", context=ctx) as span:
+ self.assertTrue(span.get_span_context().is_valid)
+ self.assertEqual(span.get_span_context(), span_context)
+ self.assertIs(span.is_recording(), False)
+
+ def test_default_tracer_context_propagation_non_recording_span(self):
+ tracer_provider = trace.NoOpTracerProvider()
+ tracer = tracer_provider.get_tracer(__name__)
+ ctx = trace.set_span_in_context(trace.INVALID_SPAN)
+ with tracer.start_span("test", context=ctx) as span:
+ self.assertFalse(span.get_span_context().is_valid)
+ self.assertIs(span, trace.INVALID_SPAN)
+
+ def test_default_tracer_context_propagation_with_invalid_context(self):
+ tracer_provider = trace.NoOpTracerProvider()
+ tracer = tracer_provider.get_tracer(__name__)
+ ctx = trace.set_span_in_context(
+ RecordingSpan(context="invalid_context") # type:
ignore[reportArgumentType]
+ )
+ with tracer.start_span("test", context=ctx) as span:
+ self.assertFalse(span.get_span_context().is_valid)
+ self.assertIs(span, trace.INVALID_SPAN)
+
def test_span(self):
with self.assertRaises(TypeError):
# pylint: disable=abstract-class-instantiated
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/opentelemetry_api-1.39.1/tests/trace/test_proxy.py
new/opentelemetry_api-1.40.0/tests/trace/test_proxy.py
--- old/opentelemetry_api-1.39.1/tests/trace/test_proxy.py 2020-02-02
01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/tests/trace/test_proxy.py 2020-02-02
01:00:00.000000000 +0100
@@ -96,8 +96,8 @@
return trace.get_current_span()
# call function before configuring tracing provider, should
- # return INVALID_SPAN from the NoOpTracer
- self.assertEqual(my_function(), trace.INVALID_SPAN)
+ # return NonRecordingSpan from the NoOpTracer
+ self.assertFalse(my_function().is_recording())
# configure tracing provider
trace.set_tracer_provider(TestProvider())
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/opentelemetry_api-1.39.1/tests/trace/test_tracer.py
new/opentelemetry_api-1.40.0/tests/trace/test_tracer.py
--- old/opentelemetry_api-1.39.1/tests/trace/test_tracer.py 2020-02-02
01:00:00.000000000 +0100
+++ new/opentelemetry_api-1.40.0/tests/trace/test_tracer.py 2020-02-02
01:00:00.000000000 +0100
@@ -79,5 +79,5 @@
def test_get_current_span(self):
with self.tracer.start_as_current_span("test") as span:
get_current_span().set_attribute("test", "test")
- self.assertEqual(span, INVALID_SPAN)
+ self.assertFalse(span.is_recording())
self.assertFalse(hasattr("span", "attributes"))