diff options
| author | Carsten Schoenert <c.schoenert@t-online.de> | 2026-05-30 09:46:16 +0200 |
|---|---|---|
| committer | git-ubuntu importer <ubuntu-devel-discuss@lists.ubuntu.com> | 2026-05-30 16:35:30 +0000 |
| commit | fa4b19cc510b29c1f5e59dce046a9ca22f38f9ad (patch) | |
| tree | 18c9240dfc58ee5e8116dee6b821a4d58e6cbded /tests | |
| parent | 5bbb8b155485e10039646141be2cf7098118e6d5 (diff) | |
1.42.1-1 (patches unapplied)HEADimport/1.42.1-1ubuntu/stonking-proposedubuntu/stonking-develubuntu/stonkingubuntu/develdebian/sid
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')
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. |
