summaryrefslogtreecommitdiff
path: root/tests
diff options
authorCarsten Schoenert <c.schoenert@t-online.de>2026-05-30 09:46:16 +0200
committergit-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com>2026-05-30 16:35:30 +0000
commitfa4b19cc510b29c1f5e59dce046a9ca22f38f9ad (patch)
tree18c9240dfc58ee5e8116dee6b821a4d58e6cbded /tests
parent5bbb8b155485e10039646141be2cf7098118e6d5 (diff)
Imported using git-ubuntu import.
Notes
Notes: * [a6c1263] New upstream version 1.42.1 * [dfb1a0f] Rebuild patch queue from patch-queue branch Dropped patch (fixed upstream): Don-t-try-using-importlib_metadata-library.patch
Diffstat (limited to 'tests')
-rw-r--r--tests/opentelemetry-docker-tests/tests/docker-compose.yml6
-rw-r--r--tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py13
-rw-r--r--tests/opentelemetry-docker-tests/tests/otlpexporter/__init__.py144
-rw-r--r--tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py71
-rw-r--r--tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_http_exporter_functional.py70
-rw-r--r--tests/opentelemetry-test-utils/pyproject.toml8
-rw-r--r--tests/opentelemetry-test-utils/src/opentelemetry/test/__init__.py13
-rw-r--r--tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py13
-rw-r--r--tests/opentelemetry-test-utils/src/opentelemetry/test/concurrency_test.py20
-rw-r--r--tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py13
-rw-r--r--tests/opentelemetry-test-utils/src/opentelemetry/test/httptest.py13
-rw-r--r--tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py19
-rw-r--r--tests/opentelemetry-test-utils/src/opentelemetry/test/mock_test_classes.py13
-rw-r--r--tests/opentelemetry-test-utils/src/opentelemetry/test/mock_textmap.py24
-rw-r--r--tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py13
-rw-r--r--tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py25
-rw-r--r--tests/opentelemetry-test-utils/src/opentelemetry/test/version/__init__.py5
-rw-r--r--tests/opentelemetry-test-utils/src/opentelemetry/test/weaver_live_check.py496
-rw-r--r--tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py16
-rw-r--r--tests/opentelemetry-test-utils/test-requirements.txt9
-rw-r--r--tests/opentelemetry-test-utils/tests/__init__.py13
-rw-r--r--tests/opentelemetry-test-utils/tests/test_base.py13
-rw-r--r--tests/opentelemetry-test-utils/tests/test_utils.py13
-rw-r--r--tests/opentelemetry-test-utils/tests/test_weaver_live_check.py192
-rw-r--r--tests/opentelemetry-test-utils/tests/testdata/registry/attributes.yaml27
-rw-r--r--tests/opentelemetry-test-utils/tests/testdata/test-policy.rego16
-rw-r--r--tests/w3c_tracecontext_validation_server.py13
27 files changed, 1022 insertions, 269 deletions
diff --git a/tests/opentelemetry-docker-tests/tests/docker-compose.yml b/tests/opentelemetry-docker-tests/tests/docker-compose.yml
index 17c5388..914bab1 100644
--- a/tests/opentelemetry-docker-tests/tests/docker-compose.yml
+++ b/tests/opentelemetry-docker-tests/tests/docker-compose.yml
@@ -1,5 +1,3 @@
-version: '3'
-
services:
otopencensus:
image: rafaeljesus/opencensus-collector:latest
@@ -8,7 +6,7 @@ services:
- "8888:8888"
- "55678:55678"
otcollector:
- image: otel/opentelemetry-collector:0.31.0
+ image: otel/opentelemetry-collector:0.149.0
ports:
- "4317:4317"
- - "4318:55681"
+ - "4318:4318"
diff --git a/tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py b/tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py
index a3c1ee2..3e3a8ee 100644
--- a/tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py
+++ b/tests/opentelemetry-docker-tests/tests/opencensus/test_opencensusexporter_functional.py
@@ -1,16 +1,5 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
from opentelemetry import trace
from opentelemetry.context import attach, detach, set_value
diff --git a/tests/opentelemetry-docker-tests/tests/otlpexporter/__init__.py b/tests/opentelemetry-docker-tests/tests/otlpexporter/__init__.py
index d4340fb..cd9a8fd 100644
--- a/tests/opentelemetry-docker-tests/tests/otlpexporter/__init__.py
+++ b/tests/opentelemetry-docker-tests/tests/otlpexporter/__init__.py
@@ -1,21 +1,32 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
+import time
from abc import ABC, abstractmethod
-from opentelemetry.context import attach, detach, set_value
+from opentelemetry.context import (
+ _SUPPRESS_INSTRUMENTATION_KEY,
+ attach,
+ detach,
+ set_value,
+)
+from opentelemetry.sdk.metrics._internal.export import (
+ MetricExportResult,
+ PeriodicExportingMetricReader,
+)
+from opentelemetry.sdk.metrics._internal.point import (
+ Metric,
+ NumberDataPoint,
+ Sum,
+)
+from opentelemetry.sdk.metrics.export import (
+ MetricsData,
+ ResourceMetrics,
+ ScopeMetrics,
+)
+from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace.export import SimpleSpanProcessor
+from opentelemetry.sdk.util.instrumentation import InstrumentationScope
class ExportStatusSpanProcessor(SimpleSpanProcessor):
@@ -29,13 +40,37 @@ class ExportStatusSpanProcessor(SimpleSpanProcessor):
detach(token)
+class ExportStatusMetricReader(PeriodicExportingMetricReader):
+ def __init__(self, exporter, **kwargs):
+ # Very short export interval for testing
+ super().__init__(exporter, export_interval_millis=1, **kwargs)
+ self.export_status = []
+
+ def _receive_metrics(self, metrics_data, timeout_millis=10_000, **kwargs):
+ token = attach(set_value(_SUPPRESS_INSTRUMENTATION_KEY, True))
+ try:
+ export_result = self._exporter.export(
+ metrics_data, timeout_millis=timeout_millis
+ )
+ self.export_status.append(export_result)
+ except Exception:
+ self.export_status.append(MetricExportResult.FAILURE)
+ finally:
+ detach(token)
+
+
class BaseTestOTLPExporter(ABC):
@abstractmethod
def get_span_processor(self):
pass
+ @abstractmethod
+ def get_metric_reader(self):
+ pass
+
# pylint: disable=no-member
def test_export(self):
+ """Test span export"""
with self.tracer.start_as_current_span("foo"):
with self.tracer.start_as_current_span("bar"):
with self.tracer.start_as_current_span("baz"):
@@ -46,3 +81,86 @@ class BaseTestOTLPExporter(ABC):
for export_status in self.span_processor.export_status:
self.assertEqual(export_status.name, "SUCCESS")
self.assertEqual(export_status.value, 0)
+
+ def test_metrics_export(self):
+ """Test metrics export from full metrics SDK pipeline"""
+ counter = self.meter.create_counter("test_counter")
+ histogram = self.meter.create_histogram("test_histogram")
+ up_down_counter = self.meter.create_up_down_counter(
+ "test_up_down_counter"
+ )
+
+ counter.add(1, {"key1": "value1"})
+ counter.add(2, {"key2": "value2"})
+ histogram.record(1.5, {"key3": "value3"})
+ histogram.record(2.5, {"key4": "value4"})
+ up_down_counter.add(3, {"key5": "value5"})
+ up_down_counter.add(-1, {"key6": "value6"})
+ self.metric_reader.force_flush(timeout_millis=5000)
+ time.sleep(0.1)
+
+ # Verify at least one export happened
+ self.assertTrue(len(self.metric_reader.export_status) >= 1)
+ # Verify all exports succeeded
+ for export_status in self.metric_reader.export_status:
+ self.assertEqual(export_status.name, "SUCCESS")
+ self.assertEqual(export_status.value, 0)
+
+ @abstractmethod
+ def test_metrics_export_batch_size_two(self):
+ """Test metrics max_export_batch_size=2 directly through exporter"""
+
+ def _create_test_metrics_data(self, num_data_points=6):
+ """Create test metrics data with specified number of data points."""
+ data_points = [
+ NumberDataPoint(
+ attributes={"key": f"value{i}"},
+ start_time_unix_nano=1000000 + i,
+ time_unix_nano=2000000 + i,
+ value=i + 1.0,
+ )
+ for i in range(num_data_points)
+ ]
+ metric = Metric(
+ name="otel_test_counter_foobar",
+ description="Test counter metric for batch verification",
+ unit="1",
+ data=Sum(
+ data_points=data_points,
+ aggregation_temporality=1, # CUMULATIVE
+ is_monotonic=True,
+ ),
+ )
+ scope_metrics = ScopeMetrics(
+ scope=InstrumentationScope(name="test_scope"),
+ metrics=[metric],
+ schema_url=None,
+ )
+ resource_metrics = ResourceMetrics(
+ resource=Resource.create({"service.name": "test-service"}),
+ scope_metrics=[scope_metrics],
+ schema_url=None,
+ )
+
+ return MetricsData(resource_metrics=[resource_metrics]), data_points
+
+ def _verify_batch_export_result(
+ self, result, data_points, batch_counter, max_batch_size=2
+ ):
+ """Verify export result and batch count for export batching tests."""
+ self.assertEqual(
+ result.name, "SUCCESS", f"Expected SUCCESS, got: {result}"
+ )
+ self.assertEqual(
+ result.value, 0, f"Expected result code 0, got: {result.value}"
+ )
+
+ expected_batches = (
+ len(data_points) + max_batch_size - 1
+ ) // max_batch_size
+ self.assertEqual(
+ batch_counter.export_call_count,
+ expected_batches,
+ f"Expected {expected_batches} export calls with max_export_batch_size={max_batch_size} and {len(data_points)} data points, "
+ f"but got {batch_counter.export_call_count} calls",
+ )
diff --git a/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py
index d48b305..4d6ba4e 100644
--- a/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py
+++ b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_grpc_exporter_functional.py
@@ -1,25 +1,38 @@
# 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.
-
-from opentelemetry import trace
+# SPDX-License-Identifier: Apache-2.0
+
+from opentelemetry import metrics, trace
+from opentelemetry.exporter.otlp.proto.grpc.metric_exporter import (
+ OTLPMetricExporter,
+)
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import (
OTLPSpanExporter,
)
+from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.test.globals_test import (
+ reset_metrics_globals,
+ reset_trace_globals,
+)
from opentelemetry.test.test_base import TestBase
-from . import BaseTestOTLPExporter, ExportStatusSpanProcessor
+from . import (
+ BaseTestOTLPExporter,
+ ExportStatusMetricReader,
+ ExportStatusSpanProcessor,
+)
+
+
+class BatchCountingGRPCExporter(OTLPMetricExporter):
+ """gRPC exporter that counts actual batch export calls for testing."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.export_call_count = 0
+
+ def _export(self, *args, **kwargs):
+ self.export_call_count += 1
+ return super()._export(*args, **kwargs)
class TestOTLPGRPCExporter(BaseTestOTLPExporter, TestBase):
@@ -29,11 +42,37 @@ class TestOTLPGRPCExporter(BaseTestOTLPExporter, TestBase):
OTLPSpanExporter(insecure=True, timeout=1)
)
+ def get_metric_reader(self):
+ return ExportStatusMetricReader(
+ OTLPMetricExporter(
+ insecure=True, timeout=1, max_export_batch_size=2
+ )
+ )
+
def setUp(self):
super().setUp()
+ reset_trace_globals()
trace.set_tracer_provider(TracerProvider())
self.tracer = trace.get_tracer(__name__)
self.span_processor = self.get_span_processor()
-
trace.get_tracer_provider().add_span_processor(self.span_processor)
+
+ reset_metrics_globals()
+ self.metric_reader = self.get_metric_reader()
+ meter_provider = MeterProvider(metric_readers=[self.metric_reader])
+ metrics.set_meter_provider(meter_provider)
+ self.meter = metrics.get_meter(__name__)
+
+ def test_metrics_export_batch_size_two(self):
+ """Test metrics max_export_batch_size=2 directly through gRPC exporter"""
+ batch_counter = BatchCountingGRPCExporter(
+ endpoint="localhost:4317", insecure=True, max_export_batch_size=2
+ )
+ metrics_data, data_points = self._create_test_metrics_data(
+ num_data_points=6
+ )
+ result = batch_counter.export(metrics_data)
+ self._verify_batch_export_result(
+ result, data_points, batch_counter, max_batch_size=2
+ )
diff --git a/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_http_exporter_functional.py b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_http_exporter_functional.py
index 59a333d..95aa772 100644
--- a/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_http_exporter_functional.py
+++ b/tests/opentelemetry-docker-tests/tests/otlpexporter/test_otlp_http_exporter_functional.py
@@ -1,25 +1,38 @@
# 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.
-
-from opentelemetry import trace
+# SPDX-License-Identifier: Apache-2.0
+
+from opentelemetry import metrics, trace
+from opentelemetry.exporter.otlp.proto.http.metric_exporter import (
+ OTLPMetricExporter,
+)
from opentelemetry.exporter.otlp.proto.http.trace_exporter import (
OTLPSpanExporter,
)
+from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.test.globals_test import (
+ reset_metrics_globals,
+ reset_trace_globals,
+)
from opentelemetry.test.test_base import TestBase
-from . import BaseTestOTLPExporter, ExportStatusSpanProcessor
+from . import (
+ BaseTestOTLPExporter,
+ ExportStatusMetricReader,
+ ExportStatusSpanProcessor,
+)
+
+
+class BatchCountingHTTPExporter(OTLPMetricExporter):
+ """HTTP exporter that counts actual batch export calls for testing."""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.export_call_count = 0
+
+ def _export(self, *args, **kwargs):
+ self.export_call_count += 1
+ return super()._export(*args, **kwargs)
class TestOTLPHTTPExporter(BaseTestOTLPExporter, TestBase):
@@ -27,11 +40,36 @@ class TestOTLPHTTPExporter(BaseTestOTLPExporter, TestBase):
def get_span_processor(self):
return ExportStatusSpanProcessor(OTLPSpanExporter())
+ def get_metric_reader(self):
+ return ExportStatusMetricReader(
+ OTLPMetricExporter(max_export_batch_size=2)
+ )
+
def setUp(self):
super().setUp()
+ reset_trace_globals()
trace.set_tracer_provider(TracerProvider())
self.tracer = trace.get_tracer(__name__)
self.span_processor = self.get_span_processor()
-
trace.get_tracer_provider().add_span_processor(self.span_processor)
+
+ reset_metrics_globals()
+ self.metric_reader = self.get_metric_reader()
+ meter_provider = MeterProvider(metric_readers=[self.metric_reader])
+ metrics.set_meter_provider(meter_provider)
+ self.meter = metrics.get_meter(__name__)
+
+ def test_metrics_export_batch_size_two(self):
+ """Test metrics max_export_batch_size=2 directly through HTTP exporter"""
+ batch_counter = BatchCountingHTTPExporter(
+ endpoint="http://localhost:4318/v1/metrics",
+ max_export_batch_size=2,
+ )
+ metrics_data, data_points = self._create_test_metrics_data(
+ num_data_points=6
+ )
+ result = batch_counter.export(metrics_data)
+ self._verify_batch_export_result(
+ result, data_points, batch_counter, max_batch_size=2
+ )
diff --git a/tests/opentelemetry-test-utils/pyproject.toml b/tests/opentelemetry-test-utils/pyproject.toml
index 924c708..b3915ba 100644
--- a/tests/opentelemetry-test-utils/pyproject.toml
+++ b/tests/opentelemetry-test-utils/pyproject.toml
@@ -8,7 +8,7 @@ dynamic = ["version"]
description = "Test utilities for OpenTelemetry unit tests"
readme = "README.rst"
license = "Apache-2.0"
-requires-python = ">=3.9"
+requires-python = ">=3.10"
authors = [
{ name = "OpenTelemetry Authors", email = "cncf-opentelemetry-contributors@lists.cncf.io" },
]
@@ -18,7 +18,6 @@ classifiers = [
"Intended Audience :: Developers",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
@@ -27,8 +26,9 @@ classifiers = [
]
dependencies = [
"asgiref ~= 3.0",
- "opentelemetry-api == 1.41.0",
- "opentelemetry-sdk == 1.41.0",
+ "opentelemetry-api == 1.42.1",
+ "opentelemetry-sdk == 1.42.1",
+ "requests ~= 2.28",
]
[project.urls]
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/__init__.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/__init__.py
index 8418886..b3de9df 100644
--- a/tests/opentelemetry-test-utils/src/opentelemetry/test/__init__.py
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/__init__.py
@@ -1,16 +1,5 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
# type: ignore
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py
index ab0215d..6b680ac 100644
--- a/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/asgitestutil.py
@@ -1,16 +1,5 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
import asyncio
from unittest import IsolatedAsyncioTestCase
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/concurrency_test.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/concurrency_test.py
index 5d178e2..25f6f35 100644
--- a/tests/opentelemetry-test-utils/src/opentelemetry/test/concurrency_test.py
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/concurrency_test.py
@@ -1,22 +1,12 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
import sys
import threading
import unittest
+from collections.abc import Callable
from functools import partial
-from typing import Callable, List, Optional, TypeVar
+from typing import TypeVar
from unittest.mock import Mock
ReturnT = TypeVar("ReturnT")
@@ -66,11 +56,11 @@ class ConcurrencyTestBase(unittest.TestCase):
def run_with_many_threads(
func_to_test: Callable[[], ReturnT],
num_threads: int = 100,
- ) -> List[ReturnT]:
+ ) -> list[ReturnT]:
"""Util to run ``func_to_test`` in ``num_threads`` concurrently"""
barrier = threading.Barrier(num_threads)
- results: List[Optional[ReturnT]] = [None] * num_threads
+ results: list[ReturnT | None] = [None] * num_threads
def thread_start(idx: int) -> None:
nonlocal results
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py
index aead836..e1cf713 100644
--- a/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/globals_test.py
@@ -1,16 +1,5 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
import unittest
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/httptest.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/httptest.py
index 84591ca..5f42a36 100644
--- a/tests/opentelemetry-test-utils/src/opentelemetry/test/httptest.py
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/httptest.py
@@ -1,16 +1,5 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
import re
import unittest
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py
index 33f1039..c2de0b7 100644
--- a/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/metrictestutil.py
@@ -1,20 +1,7 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
-from typing import Optional
-
from opentelemetry.attributes import BoundedAttributes
from opentelemetry.sdk.metrics.export import (
AggregationTemporality,
@@ -108,8 +95,8 @@ def _generate_unsupported_metric(
def _generate_histogram(
name: str,
attributes: Attributes = None,
- description: Optional[str] = None,
- unit: Optional[str] = None,
+ description: str | None = None,
+ unit: str | None = None,
) -> Metric:
if attributes is None:
attributes = BoundedAttributes(attributes={"a": 1, "b": True})
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/mock_test_classes.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/mock_test_classes.py
index 5e7fc52..680c223 100644
--- a/tests/opentelemetry-test-utils/src/opentelemetry/test/mock_test_classes.py
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/mock_test_classes.py
@@ -1,16 +1,5 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
class IterEntryPoint:
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/mock_textmap.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/mock_textmap.py
index c3e901e..09378aa 100644
--- a/tests/opentelemetry-test-utils/src/opentelemetry/test/mock_textmap.py
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/mock_textmap.py
@@ -1,18 +1,6 @@
# 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 typing
+# SPDX-License-Identifier: Apache-2.0
+
from opentelemetry import trace
from opentelemetry.context import Context
@@ -36,7 +24,7 @@ class NOOPTextMapPropagator(TextMapPropagator):
def extract(
self,
carrier: CarrierT,
- context: typing.Optional[Context] = None,
+ context: Context | None = None,
getter: Getter = default_getter,
) -> Context:
return Context()
@@ -44,7 +32,7 @@ class NOOPTextMapPropagator(TextMapPropagator):
def inject(
self,
carrier: CarrierT,
- context: typing.Optional[Context] = None,
+ context: Context | None = None,
setter: Setter = default_setter,
) -> None:
return None
@@ -63,7 +51,7 @@ class MockTextMapPropagator(TextMapPropagator):
def extract(
self,
carrier: CarrierT,
- context: typing.Optional[Context] = None,
+ context: Context | None = None,
getter: Getter = default_getter,
) -> Context:
if context is None:
@@ -88,7 +76,7 @@ class MockTextMapPropagator(TextMapPropagator):
def inject(
self,
carrier: CarrierT,
- context: typing.Optional[Context] = None,
+ context: Context | None = None,
setter: Setter = default_setter,
) -> None:
span = trace.get_current_span(context)
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py
index 912de9e..24dace2 100644
--- a/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/spantestutil.py
@@ -1,16 +1,5 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
from functools import partial
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py
index 0ec7d59..3cd1b93 100644
--- a/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/test_base.py
@@ -1,21 +1,10 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
import logging
import unittest
+from collections.abc import Sequence
from contextlib import contextmanager
-from typing import Optional, Sequence, Tuple
from opentelemetry import metrics as metrics_api
from opentelemetry import trace as trace_api
@@ -118,7 +107,7 @@ class TestBase(unittest.TestCase):
return tracer_provider, memory_exporter
@staticmethod
- def create_meter_provider(**kwargs) -> Tuple[MeterProvider, MetricReader]:
+ def create_meter_provider(**kwargs) -> tuple[MeterProvider, MetricReader]:
"""Helper to create a configured meter provider
Creates a `MeterProvider` and an `InMemoryMetricReader`.
Returns:
@@ -142,7 +131,7 @@ class TestBase(unittest.TestCase):
finally:
logging.disable(logging.NOTSET)
- def get_sorted_metrics(self, scope: Optional[str] = None):
+ def get_sorted_metrics(self, scope: str | None = None):
"""Returns recorded metrics sorted by name.
Args:
@@ -177,7 +166,7 @@ class TestBase(unittest.TestCase):
self,
metric: Metric,
expected_data_points: Sequence[DataPointT],
- est_value_delta: Optional[float] = 0,
+ est_value_delta: float | None = 0,
):
self.assertEqual(
len(expected_data_points), len(metric.data.data_points)
@@ -192,7 +181,7 @@ class TestBase(unittest.TestCase):
def is_data_points_equal(
expected_data_point: DataPointT,
data_point: DataPointT,
- est_value_delta: Optional[float] = 0,
+ est_value_delta: float | None = 0,
):
if type(expected_data_point) != type( # noqa: E721
data_point
@@ -230,7 +219,7 @@ class TestBase(unittest.TestCase):
self,
expected_data_point: DataPointT,
data_points: Sequence[DataPointT],
- est_value_delta: Optional[float] = 0,
+ est_value_delta: float | None = 0,
):
is_data_point_exist = False
for data_point in data_points:
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/version/__init__.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/version/__init__.py
index b4407ca..23888c2 100644
--- a/tests/opentelemetry-test-utils/src/opentelemetry/test/version/__init__.py
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/version/__init__.py
@@ -1 +1,4 @@
-__version__ = "0.62b0"
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+__version__ = "0.63b1"
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/weaver_live_check.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/weaver_live_check.py
new file mode 100644
index 0000000..115fa1c
--- /dev/null
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/weaver_live_check.py
@@ -0,0 +1,496 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+import functools
+import json
+import logging
+import os
+import shutil
+import socket
+import subprocess
+import tempfile
+from collections import defaultdict
+from collections.abc import Sequence
+from itertools import chain
+from typing import Any
+
+from requests import Session, post
+from requests.adapters import HTTPAdapter
+from urllib3.util.retry import Retry
+
+from opentelemetry.semconv.schemas import Schemas
+
+logger = logging.getLogger(__name__)
+
+
+def _find_free_port() -> int:
+ with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
+ sock.bind(("", 0))
+ return sock.getsockname()[1]
+
+
+def _extract_violations(report: dict) -> list:
+ """Extract and deduplicate violations from the full report.
+
+ Get all violations using python version of this jq filter:
+ [ .. | objects | select(has("live_check_result"))
+ | .live_check_result.all_advice[]?
+ | select(.level == "violation") ]
+ | group_by(.id, .message, .context, .signal_name, .signal_type)
+ | map({ ..., count: length })
+ | sort_by(-.count, .message)
+ """
+ raw: list[dict] = []
+
+ def _collect(obj: Any) -> list[dict]:
+ if isinstance(obj, dict):
+ result: list[dict] = []
+ lcr = obj.get("live_check_result")
+ if isinstance(lcr, dict):
+ advices = lcr.get("all_advice")
+ if isinstance(advices, list):
+ result.extend(
+ a for a in advices if a.get("level") == "violation"
+ )
+ for value in obj.values():
+ result.extend(_collect(value))
+ return result
+ if isinstance(obj, list):
+ return list(chain.from_iterable(_collect(item) for item in obj))
+ return []
+
+ raw = _collect(report)
+
+ groups: dict[tuple, list] = defaultdict(list)
+ for violation in raw:
+ ctx = violation.get("context")
+ key = (
+ violation.get("id"),
+ violation.get("message"),
+ json.dumps(ctx, sort_keys=True)
+ if isinstance(ctx, (dict, list))
+ else ctx,
+ violation.get("signal_name"),
+ violation.get("signal_type"),
+ )
+ groups[key].append(violation)
+
+ violations = [
+ {
+ "id": k[0],
+ "message": k[1],
+ "context": vs[0].get(
+ "context"
+ ), # preserve original dict, not JSON string
+ "signal_name": k[3],
+ "signal_type": k[4],
+ "count": len(vs),
+ }
+ for k, vs in groups.items()
+ ]
+ violations.sort(key=lambda v: (-v["count"], v.get("message") or ""))
+ return violations
+
+
+def _format_violations(violations: list) -> str:
+ """Format violations list as human-readable text."""
+ lines = []
+ for violation in violations:
+ signal = ""
+ signal_type = violation.get("signal_type")
+ signal_name = violation.get("signal_name")
+ if signal_type and signal_name:
+ signal = f" on {signal_type} '{signal_name}'"
+ elif signal_type:
+ signal = f" on {signal_type}"
+ elif signal_name:
+ signal = f" on '{signal_name}'"
+ lines.append(
+ f"- [{violation.get('id')}] {violation.get('message')} ({violation['count']} occurrence(s){signal})"
+ )
+ return "\n".join(lines)
+
+
+class LiveCheckError(AssertionError):
+ """Raised by :meth:`WeaverLiveCheck.end_and_check` when semconv violations are found.
+
+ The full :class:`LiveCheckReport` is attached as :attr:`report` for
+ structured inspection beyond the human-readable message::
+
+ with pytest.raises(LiveCheckError) as exc_info:
+ weaver.end_and_check()
+
+ err = exc_info.value
+ assert any(
+ v["id"] == "my_policy_check"
+ and v["context"]["attribute_name"] == "my.attribute"
+ for v in err.report.violations
+ )
+ """
+
+ def __init__(self, message: str, report: "LiveCheckReport") -> None:
+ super().__init__(message)
+ self.report = report
+
+
+class LiveCheckReport:
+ """The result of a weaver live-check run.
+
+ Provides structured access to violations and the full raw JSON report.
+
+ See https://github.com/open-telemetry/weaver/tree/main/crates/weaver_live_check#output
+ for the full report structure.
+
+ Example — asserting on metrics statistics::
+
+ report = weaver.end()
+ seen = report["statistics"]["seen_registry_metrics"]
+ assert seen.get("http.server.request.duration") == 1
+
+ Example — asserting on violations::
+
+ report = weaver.end()
+ assert any(
+ v["id"] == "my_policy_check"
+ and v["context"]["attribute_name"] == "my.attribute"
+ for v in report.violations
+ )
+ """
+
+ def __init__(self, report: dict[str, Any]) -> None:
+ self._report = report
+
+ @functools.cached_property
+ def violations(self) -> list[dict[str, Any]]:
+ """Deduplicated list of semconv violations found in the report.
+
+ Each item is a dict with keys: ``id``, ``message``, ``context``
+ (the raw context dict from weaver, e.g. ``{"attribute_name": "foo"}``),
+ ``signal_name``, ``signal_type``, ``count``.
+ """
+ return _extract_violations(self._report)
+
+ def __getitem__(self, key: str) -> Any:
+ return self._report[key]
+
+ def get(self, key: str, default: Any = None) -> Any:
+ return self._report.get(key, default)
+
+ def __contains__(self, key: object) -> bool:
+ return key in self._report
+
+ def __repr__(self) -> str:
+ num_violations = len(self.violations)
+ return f"LiveCheckReport({num_violations} violation{'s' if num_violations != 1 else ''})"
+
+
+# NOTE: WeaverLiveCheck is experimental and its API is subject to change.
+class WeaverLiveCheck:
+ """Runs ``weaver registry live-check`` as a subprocess and validates
+ OTLP telemetry against OpenTelemetry semantic conventions.
+
+ .. note::
+ This class is experimental and its API is subject to change without notice.
+
+
+ Requires the ``weaver`` binary on PATH:
+ https://github.com/open-telemetry/weaver/releases
+
+ Typical use as a context manager::
+
+ def test_my_telemetry(self):
+ with WeaverLiveCheck() as weaver:
+ exporter = OTLPSpanExporter(
+ endpoint=weaver.otlp_endpoint, insecure=True
+ )
+ # ... configure provider, emit telemetry ...
+ provider.force_flush()
+
+ # Signals weaver to stop, raises LiveCheckError listing violations
+ # if any, or returns a LiveCheckReport on success.
+ report = weaver.end_and_check()
+ # __exit__ calls close(), which is idempotent if end_and_check() was already called
+
+ Use :meth:`end` when you need the full :class:`LiveCheckReport`
+ regardless of whether violations were found — for example, to assert that
+ specific metrics were observed or to inspect violation fields directly::
+
+ with WeaverLiveCheck() as weaver:
+ # ... configure provider, emit telemetry ...
+ provider.force_flush()
+ report = weaver.end()
+
+ seen_metrics = report["statistics"]["seen_registry_metrics"]
+ assert seen_metrics.get("http.server.request.duration") == 1
+
+ Lifecycle:
+ - :meth:`start` — launches weaver and waits for it to become ready.
+ - :attr:`otlp_endpoint` — gRPC OTLP endpoint to point exporters at.
+ - :meth:`end` — signals weaver to stop and always returns a
+ :class:`LiveCheckReport`. Never raises for semconv violations; use
+ this when you want to write your own assertions.
+ - :meth:`end_and_check` — signals weaver to stop and raises
+ :class:`LiveCheckError` with a human-readable violation list and the
+ full report attached if weaver exits non-zero. Returns a
+ :class:`LiveCheckReport` on success.
+ - :meth:`close` — stops weaver if not already stopped and terminates the
+ process. Never raises for semconv violations. Idempotent; safe to
+ call even if :meth:`end_and_check` or :meth:`end` was already called.
+ """
+
+ def __init__(
+ self,
+ registry: str | None = None,
+ schema_version: str | None = None,
+ policies_dir: str | None = None,
+ inactivity_timeout: int = 30,
+ otlp_port: int = 0,
+ admin_port: int = 0,
+ extra_args: Sequence[str] | None = None,
+ ):
+ """Build the ``weaver registry live-check`` command.
+
+ ``extra_args`` is appended verbatim to the weaver command after the
+ managed flags (``--registry``, ``--otlp-grpc-port``, etc.) and lets
+ callers pass additional weaver options — for example ``["--quiet"]``
+ or ``["--skip-policies"]`` — without subclassing.
+ """
+ weaver_bin = shutil.which("weaver")
+ if not weaver_bin:
+ raise RuntimeError(
+ "weaver binary not found on PATH. "
+ "Install it from https://github.com/open-telemetry/weaver/releases"
+ )
+
+ self._otlp_port = otlp_port or _find_free_port()
+ self._admin_port = admin_port or _find_free_port()
+ self._ready = False
+ self._stopped = False
+ self._process: subprocess.Popen[bytes] | None = None
+ self._stdout_path: str | None = None
+ self._stderr_path: str | None = None
+
+ command = [
+ weaver_bin,
+ "registry",
+ "live-check",
+ f"--inactivity-timeout={inactivity_timeout}",
+ f"--otlp-grpc-port={self._otlp_port}",
+ f"--admin-port={self._admin_port}",
+ "--output=http",
+ "--format=json",
+ ]
+
+ if policies_dir:
+ command += ["--advice-policies", os.path.abspath(policies_dir)]
+
+ if registry is None:
+ if schema_version is None:
+ schema_version = list(Schemas)[-1].value.rsplit("/", 1)[-1]
+ registry = f"https://github.com/open-telemetry/semantic-conventions/archive/refs/tags/v{schema_version}.tar.gz[model]"
+ elif os.path.isdir(registry):
+ registry = os.path.abspath(registry)
+
+ command += ["--registry", registry]
+
+ if extra_args:
+ command.extend(extra_args)
+
+ self._command = command
+ logger.debug("Weaver command: %s", command)
+
+ def __enter__(self) -> "WeaverLiveCheck":
+ return self.start()
+
+ def __exit__(self, exc_type: Any, *_: Any) -> None:
+ if exc_type is not None:
+ self._stopped = True
+ self.close()
+
+ def start(self) -> "WeaverLiveCheck":
+ logger.debug("Starting WeaverLiveCheck process...")
+ # Redirect weaver's stdout/stderr to tempfiles
+ stdout_fd, self._stdout_path = tempfile.mkstemp(
+ prefix="weaver-stdout-", suffix=".log"
+ )
+ stderr_fd, self._stderr_path = tempfile.mkstemp(
+ prefix="weaver-stderr-", suffix=".log"
+ )
+ try:
+ self._process = subprocess.Popen( # pylint: disable=consider-using-with
+ self._command,
+ stdout=stdout_fd,
+ stderr=stderr_fd,
+ )
+ finally:
+ os.close(stdout_fd)
+ os.close(stderr_fd)
+ try:
+ self._wait_for_ready()
+ self._ready = True
+ except Exception as exc: # pylint: disable=broad-except
+ logs = self._read_weaver_logs()
+ logger.error(
+ "WeaverLiveCheck did not start: %s, logs: %s", exc, logs
+ )
+ raise
+ return self
+
+ def _wait_for_ready(self) -> None:
+ retry = Retry(
+ total=10,
+ backoff_factor=1,
+ backoff_max=1,
+ # Any non-2xx response from /health means weaver isn't ready yet.
+ status_forcelist=list(range(300, 600)),
+ raise_on_status=True,
+ allowed_methods=["GET"],
+ )
+ session = Session()
+ session.mount("http://", HTTPAdapter(max_retries=retry))
+ try:
+ session.get(
+ f"http://localhost:{self._admin_port}/health", timeout=5
+ )
+ except Exception as exc: # pylint: disable=broad-except
+ if self._process is not None and self._process.poll() is not None:
+ raise RuntimeError(
+ f"WeaverLiveCheck process exited unexpectedly (code {self._process.returncode})"
+ ) from exc
+ raise TimeoutError(
+ "WeaverLiveCheck did not become ready in time"
+ ) from exc
+
+ @property
+ def otlp_endpoint(self) -> str:
+ return f"http://localhost:{self._otlp_port}"
+
+ def _do_stop(self, timeout: int) -> tuple["LiveCheckReport", int]:
+ """POST /stop, wait for the process to exit, return (report, exit_code).
+
+ Raises for infrastructure errors (HTTP failure, process communication).
+ Never raises for semconv violations.
+ """
+ if not self._ready:
+ raise RuntimeError(
+ "WeaverLiveCheck process did not start successfully"
+ )
+ try:
+ response = post(
+ f"http://localhost:{self._admin_port}/stop", timeout=5
+ )
+ response.raise_for_status()
+ report = LiveCheckReport(response.json())
+ assert self._process is not None
+ exit_code = self._process.wait(timeout=timeout)
+ except Exception as exc: # pylint: disable=broad-except
+ logs = self._read_weaver_logs()
+ logger.error(
+ "Error communicating with weaver: %s, logs: %s", exc, logs
+ )
+ raise
+ return report, exit_code
+
+ def end(self, timeout: int = 30) -> "LiveCheckReport":
+ """Signal weaver to stop and return the full :class:`LiveCheckReport`.
+
+ Never raises for semconv violations — use this when you want to write
+ your own assertions against :attr:`LiveCheckReport.violations` or the
+ raw report data.
+
+ Raises :exc:`RuntimeError` for infrastructure problems (weaver failed
+ to start, HTTP communication error, etc.).
+
+ See https://github.com/open-telemetry/weaver/tree/main/crates/weaver_live_check#output
+ for the report structure.
+ """
+ if self._stopped:
+ logger.warning(
+ "end() called after weaver already stopped; returning empty report"
+ )
+ return LiveCheckReport({})
+ self._stopped = True
+ report, _ = self._do_stop(timeout)
+ return report
+
+ def end_and_check(self, timeout: int = 30) -> "LiveCheckReport":
+ """Signal weaver to stop and assert no semconv violations were found.
+
+ Returns the :class:`LiveCheckReport` when weaver exits successfully
+ (exit code 0).
+
+ Does **not** return if weaver exits with a non-zero status — raises
+ :exc:`LiveCheckError` (a subclass of :exc:`AssertionError`) with a
+ human-readable list of violations and the full :class:`LiveCheckReport`
+ attached as :attr:`LiveCheckError.report`.
+ Use :meth:`end` if you need the report regardless of violations.
+
+ Raises :exc:`RuntimeError` for infrastructure problems (weaver failed
+ to start, HTTP communication error, etc.).
+ """
+ if self._stopped:
+ logger.warning(
+ "end_and_check() called after weaver already stopped; returning empty report"
+ )
+ return LiveCheckReport({})
+ self._stopped = True
+ report, exit_code = self._do_stop(timeout)
+ if exit_code == 0:
+ # Success — no violations found, no errors communicating with weaver
+ return report
+ raise LiveCheckError(
+ f"Semconv violations found:\n{_format_violations(report.violations)}",
+ report,
+ )
+
+ def _read_weaver_logs(self) -> str | None:
+ if self._process is None:
+ return None
+ try:
+ if self._process.poll() is None:
+ self._process.kill()
+ try:
+ self._process.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ pass
+
+ def _read(path: str | None) -> str:
+ if path is None or not os.path.exists(path):
+ return ""
+ with open(path, "rb") as fp:
+ return fp.read().decode(errors="replace")
+
+ return f"{_read(self._stdout_path)}\n{_read(self._stderr_path)}"
+ except Exception as exc: # pylint: disable=broad-except
+ logger.error("Could not get weaver logs: %s", exc)
+ return None
+
+ def close(self) -> None:
+ """Stop weaver and clean up the process.
+
+ If weaver has not been stopped yet, sends the ``/stop`` signal and
+ waits for the process to exit. Never raises for semconv violations.
+ Idempotent — safe to call multiple times or after :meth:`end` /
+ :meth:`end_and_check` has already been called.
+ """
+ if not self._stopped:
+ self._stopped = True
+ if self._ready:
+ try:
+ self._do_stop(timeout=30)
+ except Exception as exc: # pylint: disable=broad-except
+ logger.debug("Error stopping weaver during close: %s", exc)
+ if self._process and self._process.poll() is None:
+ self._process.terminate()
+ try:
+ self._process.wait(timeout=5)
+ except subprocess.TimeoutExpired:
+ self._process.kill()
+ for path in (self._stdout_path, self._stderr_path):
+ if path is not None:
+ try:
+ os.unlink(path)
+ except OSError:
+ pass
+ self._stdout_path = None
+ self._stderr_path = None
diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py
index 908b1d4..b12482b 100644
--- a/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py
+++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/wsgitestutil.py
@@ -1,16 +1,5 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
import io
import wsgiref.util as wsgiref_util
@@ -48,7 +37,8 @@ class WsgiTestBase(TestBase):
trace_id = trace.format_trace_id(span.get_span_context().trace_id)
span_id = trace.format_span_id(span.get_span_context().span_id)
+ trace_flags = span.get_span_context().trace_flags
self.assertEqual(
- f"00-{trace_id}-{span_id}-01",
+ f"00-{trace_id}-{span_id}-{trace_flags:02x}",
headers["traceresponse"],
)
diff --git a/tests/opentelemetry-test-utils/test-requirements.txt b/tests/opentelemetry-test-utils/test-requirements.txt
index 854e732..7cb9414 100644
--- a/tests/opentelemetry-test-utils/test-requirements.txt
+++ b/tests/opentelemetry-test-utils/test-requirements.txt
@@ -1,15 +1,18 @@
asgiref==3.7.2
-importlib-metadata==6.11.0
iniconfig==2.0.0
packaging==24.0
pluggy==1.6.0
py-cpuinfo==9.0.0
pytest==7.4.4
tomli==2.0.1
-typing_extensions==4.10.0
+typing_extensions==4.12.0
wrapt==1.16.0
-zipp==3.19.2
-e opentelemetry-api
-e opentelemetry-sdk
-e opentelemetry-semantic-conventions
-e tests/opentelemetry-test-utils
+# these are required for weaver integration tests, we're running that only on linux / CPython
+# because of lack of support for gRPC wheels on some platforms
+./opentelemetry-proto ; platform_python_implementation != 'PyPy'
+./exporter/opentelemetry-exporter-otlp-proto-common ; platform_python_implementation != 'PyPy'
+./exporter/opentelemetry-exporter-otlp-proto-grpc ; platform_python_implementation != 'PyPy'
diff --git a/tests/opentelemetry-test-utils/tests/__init__.py b/tests/opentelemetry-test-utils/tests/__init__.py
index b0a6f42..e57cf4a 100644
--- a/tests/opentelemetry-test-utils/tests/__init__.py
+++ b/tests/opentelemetry-test-utils/tests/__init__.py
@@ -1,13 +1,2 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
diff --git a/tests/opentelemetry-test-utils/tests/test_base.py b/tests/opentelemetry-test-utils/tests/test_base.py
index 92b83b9..87dbcc8 100644
--- a/tests/opentelemetry-test-utils/tests/test_base.py
+++ b/tests/opentelemetry-test-utils/tests/test_base.py
@@ -1,16 +1,5 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
from opentelemetry.test.test_base import TestBase
diff --git a/tests/opentelemetry-test-utils/tests/test_utils.py b/tests/opentelemetry-test-utils/tests/test_utils.py
index dbf0688..bcae388 100644
--- a/tests/opentelemetry-test-utils/tests/test_utils.py
+++ b/tests/opentelemetry-test-utils/tests/test_utils.py
@@ -1,16 +1,5 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
from opentelemetry.test import TestCase
diff --git a/tests/opentelemetry-test-utils/tests/test_weaver_live_check.py b/tests/opentelemetry-test-utils/tests/test_weaver_live_check.py
new file mode 100644
index 0000000..8275d28
--- /dev/null
+++ b/tests/opentelemetry-test-utils/tests/test_weaver_live_check.py
@@ -0,0 +1,192 @@
+# Copyright The OpenTelemetry Authors
+# SPDX-License-Identifier: Apache-2.0
+
+"""Live-check tests using Weaver to validate SDK telemetry against semconv.
+
+Requires the `weaver` binary on PATH:
+ https://github.com/open-telemetry/weaver/releases
+"""
+
+import os
+import shutil
+import unittest
+
+from opentelemetry.sdk.resources import SERVICE_NAME, Resource
+from opentelemetry.sdk.trace import TracerProvider
+from opentelemetry.sdk.trace.export import BatchSpanProcessor
+from opentelemetry.test.weaver_live_check import (
+ LiveCheckError,
+ LiveCheckReport,
+ WeaverLiveCheck,
+)
+
+try:
+ from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import ( # pylint: disable=no-name-in-module
+ OTLPSpanExporter,
+ )
+
+ _HAS_GRPC = True
+except ImportError:
+ _HAS_GRPC = False
+
+_TESTDATA_DIR = os.path.join(os.path.dirname(__file__), "testdata")
+_REGISTRY_DIR = os.path.join(_TESTDATA_DIR, "registry")
+
+
+def _make_provider(otlp_endpoint: str) -> TracerProvider:
+ resource = Resource.create({SERVICE_NAME: "test-service"})
+ exporter = OTLPSpanExporter(endpoint=otlp_endpoint, insecure=True)
+ provider = TracerProvider(resource=resource)
+ provider.add_span_processor(BatchSpanProcessor(exporter))
+ return provider
+
+
+@unittest.skipUnless(
+ _HAS_GRPC,
+ "grpc exporter not installed",
+)
+@unittest.skipUnless(
+ shutil.which("weaver") is not None,
+ "weaver binary not found on PATH — install from https://github.com/open-telemetry/weaver/releases",
+)
+class TestSDKInitLiveCheck(unittest.TestCase):
+ def test_end_and_check_no_violations(self):
+ """end_and_check() returns a LiveCheckReport with no violations on conformant telemetry."""
+ with WeaverLiveCheck(registry=_REGISTRY_DIR) as weaver:
+ provider = _make_provider(weaver.otlp_endpoint)
+ with provider.get_tracer("test-tracer").start_as_current_span(
+ "test-span"
+ ):
+ pass
+ provider.force_flush()
+ report = weaver.end_and_check()
+
+ self.assertIsInstance(report, LiveCheckReport)
+ self.assertEqual(report.violations, [])
+
+ def test_end_and_check_raises_on_violations(self):
+ """end_and_check() raises LiveCheckError with the report attached."""
+ with WeaverLiveCheck(
+ registry=_REGISTRY_DIR, policies_dir=_TESTDATA_DIR
+ ) as weaver:
+ provider = _make_provider(weaver.otlp_endpoint)
+ with provider.get_tracer("test-tracer").start_as_current_span(
+ "test-span"
+ ) as span:
+ span.set_attribute("never.use.this.attribute", "bad value")
+
+ provider.force_flush()
+
+ with self.assertRaises(LiveCheckError) as cm:
+ weaver.end_and_check()
+
+ # Human-readable message lists the violation
+ self.assertIn(
+ "never.use.this.attribute is forbidden by this bogus policy",
+ str(cm.exception),
+ )
+ # Structured report is attached for programmatic inspection
+ self.assertTrue(
+ any(
+ v["id"] == "test_check"
+ and v["context"].get("attribute_name")
+ == "never.use.this.attribute"
+ for v in cm.exception.report.violations
+ )
+ )
+
+ def test_end_no_violations(self):
+ """end() returns a LiveCheckReport with no violations on conformant telemetry."""
+ with WeaverLiveCheck(registry=_REGISTRY_DIR) as weaver:
+ provider = _make_provider(weaver.otlp_endpoint)
+ with provider.get_tracer("test-tracer").start_as_current_span(
+ "test-span"
+ ):
+ pass
+ provider.force_flush()
+ report = weaver.end()
+
+ self.assertIsInstance(report, LiveCheckReport)
+ self.assertEqual(report.violations, [])
+ # LiveCheckReport supports dict-style access to the raw report data
+ self.assertIn("statistics", report)
+ self.assertIsNotNone(report.get("statistics"))
+ self.assertIsNone(report.get("nonexistent"))
+
+ def test_end_with_violations(self):
+ """end() returns a LiveCheckReport with violations without raising."""
+ with WeaverLiveCheck(
+ registry=_REGISTRY_DIR, policies_dir=_TESTDATA_DIR
+ ) as weaver:
+ provider = _make_provider(weaver.otlp_endpoint)
+ with provider.get_tracer("test-tracer").start_as_current_span(
+ "test-span"
+ ) as span:
+ span.set_attribute("never.use.this.attribute", "bad value")
+
+ provider.force_flush()
+ report = weaver.end()
+
+ self.assertIsInstance(report, LiveCheckReport)
+ # Check the violation id (maps to advice_type in the rego policy)
+ self.assertTrue(
+ any(v["id"] == "test_check" for v in report.violations)
+ )
+ # Check the structured context identifies the offending attribute by name
+ self.assertTrue(
+ any(
+ isinstance(v["context"], dict)
+ and v["context"].get("attribute_name")
+ == "never.use.this.attribute"
+ for v in report.violations
+ )
+ )
+
+ def test_report_span_statistics(self):
+ """The full report exposes span counts and individual span samples."""
+ with WeaverLiveCheck(registry=_REGISTRY_DIR) as weaver:
+ provider = _make_provider(weaver.otlp_endpoint)
+ with provider.get_tracer("test-tracer").start_as_current_span(
+ "test-span"
+ ):
+ pass
+ provider.force_flush()
+ report = weaver.end()
+
+ # Individual spans are accessible in report["samples"], each entry
+ # with a "span" key containing the span data.
+ span_samples = [
+ s["span"] for s in report.get("samples", []) if "span" in s
+ ]
+ self.assertTrue(
+ any(s["name"] == "test-span" for s in span_samples),
+ f"Expected 'test-span' in samples, got: {[s['name'] for s in span_samples]}",
+ )
+
+
+@unittest.skipUnless(
+ _HAS_GRPC,
+ "grpc exporter not installed",
+)
+@unittest.skipUnless(
+ shutil.which("weaver") is not None,
+ "weaver binary not found on PATH — install from https://github.com/open-telemetry/weaver/releases",
+)
+class TestOutputCapture(unittest.TestCase):
+ """weaver's stdout/stderr is captured to tempfiles, not PIPE, so large
+ diagnostic output can't deadlock the subprocess."""
+
+ def test_does_not_deadlock_on_large_diagnostic_output(self):
+ """`--debug --debug` makes weaver dump trace logs that exceed the 64KB
+ PIPE buffer; with tempfile capture the subprocess still exits cleanly."""
+ with WeaverLiveCheck(
+ registry=_REGISTRY_DIR, extra_args=["--debug", "--debug"]
+ ) as weaver:
+ provider = _make_provider(weaver.otlp_endpoint)
+ with provider.get_tracer("test-tracer").start_as_current_span(
+ "test-span"
+ ):
+ pass
+ provider.force_flush()
+ report = weaver.end()
+ self.assertIsInstance(report, LiveCheckReport)
diff --git a/tests/opentelemetry-test-utils/tests/testdata/registry/attributes.yaml b/tests/opentelemetry-test-utils/tests/testdata/registry/attributes.yaml
new file mode 100644
index 0000000..d4a5e30
--- /dev/null
+++ b/tests/opentelemetry-test-utils/tests/testdata/registry/attributes.yaml
@@ -0,0 +1,27 @@
+# this is a test registry, used to save up time on weaver loading semconv from GH repo
+# and preventing flaky tests resulting from unpredictable network/performance in CI
+groups:
+ - id: registry.test
+ type: attribute_group
+ brief: Minimal registry for WeaverLiveCheck self-tests.
+ attributes:
+ - id: service.name
+ type: string
+ brief: The name of the service.
+ stability: stable
+ examples: ["test-service"]
+ - id: telemetry.sdk.language
+ type: string
+ brief: The language of the telemetry SDK.
+ stability: stable
+ examples: ["python"]
+ - id: telemetry.sdk.name
+ type: string
+ brief: The name of the telemetry SDK.
+ stability: stable
+ examples: ["opentelemetry"]
+ - id: telemetry.sdk.version
+ type: string
+ brief: The version string of the telemetry SDK.
+ stability: stable
+ examples: ["1.0.0"]
diff --git a/tests/opentelemetry-test-utils/tests/testdata/test-policy.rego b/tests/opentelemetry-test-utils/tests/testdata/test-policy.rego
new file mode 100644
index 0000000..de8e9a2
--- /dev/null
+++ b/tests/opentelemetry-test-utils/tests/testdata/test-policy.rego
@@ -0,0 +1,16 @@
+package live_check_advice
+
+import rego.v1
+
+deny contains result if {
+ input.sample.attribute.name == "never.use.this.attribute"
+ result := {
+ "type": "advice",
+ "advice_type": "test_check",
+ "advice_level": "violation",
+ "context": {
+ "attribute_name": input.sample.attribute.name,
+ },
+ "message": "never.use.this.attribute is forbidden by this bogus policy",
+ }
+}
diff --git a/tests/w3c_tracecontext_validation_server.py b/tests/w3c_tracecontext_validation_server.py
index 732ca1e..4b44d68 100644
--- a/tests/w3c_tracecontext_validation_server.py
+++ b/tests/w3c_tracecontext_validation_server.py
@@ -1,16 +1,5 @@
# 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.
+# SPDX-License-Identifier: Apache-2.0
"""
This server is intended to be used with the W3C tracecontext validation
Service. It implements the APIs needed to be exercised by the test bed.