diff --git a/.changelog/5740.added b/.changelog/5740.added new file mode 100644 index 00000000000..a6a1d714a23 --- /dev/null +++ b/.changelog/5740.added @@ -0,0 +1 @@ +`opentelemetry-docker-tests`: add declarative config end-to-end tests diff --git a/.github/workflows/generate_workflows.py b/.github/workflows/generate_workflows.py index 8e4ccd3c87c..bee7e00198e 100644 --- a/.github/workflows/generate_workflows.py +++ b/.github/workflows/generate_workflows.py @@ -1,4 +1,5 @@ from collections import defaultdict +from configparser import ConfigParser from pathlib import Path from re import compile as re_compile @@ -122,16 +123,30 @@ def get_misc_job_datas(tox_envs: list) -> list: return [tox_env for tox_env in tox_envs if not any(pattern.match(tox_env) for pattern in regex_patterns)] +def get_contrib_job_datas(tox_ini_path: Path, job_datas: list) -> list: + """Return the jobs whose tox env installs packages from a contrib repo checkout.""" + tox_ini = ConfigParser(interpolation=None, strict=False) + tox_ini.read(tox_ini_path) + + return [ + job_data + for job_data in job_datas + if "{env:CONTRIB_REPO_" in tox_ini.get(f"testenv:{job_data}", "deps", fallback="") + ] + + def _generate_workflow( job_datas: list, template_name: str, output_dir: Path, + **template_kwargs, ) -> None: env = Environment(loader=FileSystemLoader(Path(__file__).parent.joinpath("templates"))) with open(output_dir.joinpath(f"{template_name}.yml"), "w") as yml_file: yml_file.write( env.get_template(f"{template_name}.yml.j2").render( job_datas=job_datas, + **template_kwargs, ) ) yml_file.write("\n") @@ -160,10 +175,12 @@ def generate_misc_workflow( tox_ini_path: Path, workflow_directory_path: Path, ) -> None: + misc_job_datas = get_misc_job_datas(get_tox_envs(tox_ini_path)) _generate_workflow( - get_misc_job_datas(get_tox_envs(tox_ini_path)), + misc_job_datas, "misc", workflow_directory_path, + contrib_job_datas=get_contrib_job_datas(tox_ini_path, misc_job_datas), ) diff --git a/.github/workflows/misc.yml b/.github/workflows/misc.yml index 78234e8cfcb..dec23dc149a 100644 --- a/.github/workflows/misc.yml +++ b/.github/workflows/misc.yml @@ -23,6 +23,7 @@ env: PIP_EXISTS_ACTION: w CONTRIB_REPO_UTIL_HTTP: ${{ github.workspace }}/opentelemetry-python-contrib/util/opentelemetry-util-http CONTRIB_REPO_INSTRUMENTATION: ${{ github.workspace }}/opentelemetry-python-contrib/opentelemetry-instrumentation + CONTRIB_REPO_DISTRO: ${{ github.workspace }}/opentelemetry-python-contrib/opentelemetry-distro CONTRIB_REPO_INSTRUMENTATION_REQUESTS: ${{ github.workspace }}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests CONTRIB_REPO_INSTRUMENTATION_WSGI: ${{ github.workspace }}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi CONTRIB_REPO_INSTRUMENTATION_FLASK: ${{ github.workspace }}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask @@ -216,6 +217,35 @@ jobs: - name: Run tests run: uvx --with tox-uv tox -e docker-tests-opencensus + docker-tests-declconfig: + name: docker-tests-declconfig + runs-on: ubuntu-latest + timeout-minutes: 30 + steps: + - name: Checkout repo @ SHA - ${{ github.sha }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Checkout contrib repo @ ${{ env.CONTRIB_REPO_SHA }} + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + repository: open-telemetry/opentelemetry-python-contrib + ref: ${{ env.CONTRIB_REPO_SHA }} + path: opentelemetry-python-contrib + + - name: Set up Python 3.11 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.11" + + - name: Set up uv + uses: astral-sh/setup-uv@c771a70e6277c0a99b617c7a806ffedaca235ff9 # v9.0.0 + + - name: Run tests + run: uvx --with tox-uv tox -e docker-tests-declconfig + public-symbols-check: name: public-symbols-check runs-on: ubuntu-latest diff --git a/.github/workflows/templates/misc.yml.j2 b/.github/workflows/templates/misc.yml.j2 index 193e5bc864a..451eec1a263 100644 --- a/.github/workflows/templates/misc.yml.j2 +++ b/.github/workflows/templates/misc.yml.j2 @@ -23,6 +23,7 @@ env: PIP_EXISTS_ACTION: w CONTRIB_REPO_UTIL_HTTP: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/util/opentelemetry-util-http CONTRIB_REPO_INSTRUMENTATION: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/opentelemetry-instrumentation + CONTRIB_REPO_DISTRO: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/opentelemetry-distro CONTRIB_REPO_INSTRUMENTATION_REQUESTS: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-requests CONTRIB_REPO_INSTRUMENTATION_WSGI: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-wsgi CONTRIB_REPO_INSTRUMENTATION_FLASK: ${% raw %}{{ github.workspace }}{% endraw %}/opentelemetry-python-contrib/instrumentation/opentelemetry-instrumentation-flask @@ -56,7 +57,7 @@ jobs: with: persist-credentials: false{% if job_data == "public-symbols-check" %} fetch-depth: 0{% endif %} - {%- if job_data == "tracecontext" %} + {%- if job_data in contrib_job_datas %} - name: Checkout contrib repo @ ${% raw %}{{ env.CONTRIB_REPO_SHA }}{% endraw %} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/dev-requirements.txt b/dev-requirements.txt index ec24ee8db4c..2ccc908e376 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -20,3 +20,4 @@ psutil==7.2.2 GitPython==3.1.59 pre-commit==3.7.0 ruff==0.16.1 +testcontainers==4.13.3 diff --git a/tests/opentelemetry-docker-tests/tests/conftest.py b/tests/opentelemetry-docker-tests/tests/conftest.py deleted file mode 100644 index c066112ab24..00000000000 --- a/tests/opentelemetry-docker-tests/tests/conftest.py +++ /dev/null @@ -1,27 +0,0 @@ -# Copyright The OpenTelemetry Authors -# SPDX-License-Identifier: Apache-2.0 - -from __future__ import annotations - -from collections.abc import Iterator - -import pytest -from prometheus_client import CollectorRegistry, start_http_server - -_PROMETHEUS_PORT = 9464 - - -@pytest.fixture(scope="session") -def prometheus_registry() -> CollectorRegistry: - return CollectorRegistry() - - -@pytest.fixture(scope="session", autouse=True) -def _prometheus_scrape_target(prometheus_registry: CollectorRegistry) -> Iterator[None]: - """Keep a Prometheus HTTP endpoint up on :9464 for the whole docker-test session.""" - httpd, _thread = start_http_server(port=_PROMETHEUS_PORT, addr="0.0.0.0", registry=prometheus_registry) - try: - yield - finally: - httpd.shutdown() - httpd.server_close() diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/README.md b/tests/opentelemetry-docker-tests/tests/declconfig/README.md new file mode 100644 index 00000000000..8cb2bf69a8d --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/README.md @@ -0,0 +1,86 @@ +# Declarative configuration end-to-end tests + +Each scenario runs an app under `opentelemetry-instrument`, configured only by +a declarative config file. The app exports to a real OpenTelemetry Collector, +which forwards everything to an OTLP receiver in the test process (the sink). + +```mermaid +flowchart LR + config[otel-config.yaml] -. configures .-> app["run.py
(opentelemetry-instrument)"] + app -- OTLP --> collector["Collector
(Docker)"] + collector -- otlphttp --> sink["Sink
(in the test process)"] + sink --> checks["pytest
until / expect"] +``` + +Run the suite with: + +```sh +uv run tox -e docker-tests-declconfig +``` + +## Scenarios + +Every directory under `scenarios/` is one test case: + +| File | Purpose | +| ----------------------- | --------------------------------------------------------------------------- | +| `scenario.yaml` | Description, when to stop (`until`) and what to check (`expect`). | +| `otel-config.yaml` | The SDK config, passed as `OTEL_CONFIG_FILE`. Can be `otel-config.json`. | +| `run.py` | The app. Uses only the OpenTelemetry API. | +| `collector-config.yaml` | Optional. Defaults to the shared [`collector-config.yaml`](collector-config.yaml). | + +The shared collector receives OTLP on gRPC `4317` and HTTP `4318`. Configs +reach it at `://${OTEL_COLLECTOR_ENDPOINT}:`. A custom collector +config must enable `health_check` on `0.0.0.0:13133` and export with +`otlphttp` to `${env:OTLP_SINK_ENDPOINT}`. + +The app doesn't inherit `OTEL_*` variables from the test environment. + +## `scenario.yaml` + +The full format is in [`scenario.schema.json`](scenario.schema.json). + +```yaml +description: Exports a span over OTLP/HTTP. +timeout: 30 # seconds, default 30 +env: {MY_VAR: value} # extra environment variables for the app +until: # all must hold + - spans: {name: my-span, min_count: 1} + - metric: {name: requests, field: value, min: 5} # field: value | count | sum + - logs: {body_pattern: "user .* in"} + - exited: {} # the app exited cleanly +expect: + - resource: # every received item must match + schema_url: https://opentelemetry.io/schemas/1.26.0 + attributes: {values: {service.name: my-service}} + - spans: + name: my-span + kind: INTERNAL + parent: {name: outer} # nested span matcher + attributes: {values: {http.method: GET}, strict: true} + count: 1 # or min_count (default 1) / max_count + - metric: + name: requests + type: sum + temporality: CUMULATIVE + data_point: {value: {gte: 3}, exemplar_count: {gte: 1}} + - logs: + body: {pattern: "user .* in"} + severity_number: WARN + span: {name: handler} + - output: + stream: stderr # stdout | stderr + pattern: Unsupported file_format + exists: true # false asserts no match +``` + +`count: 0` asserts that nothing matches. Pair it with `until: [exited: {}]` +when nothing should be exported at all. + +Values are type strict: `5` matches only an int, `5.0` only a double and +`"5"` only a string. Use `{double: 5}` or `{kvlist: {...}}` to set a type +explicitly. Instead of a literal, a matcher can use the operators `pattern`, +`exists`, `gt`, `gte`, `lt` and `lte`. + +A failure lists each unmet entry with its closest candidates, plus the app's +stdout and stderr. diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/__init__.py b/tests/opentelemetry-docker-tests/tests/declconfig/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/_expect.py b/tests/opentelemetry-docker-tests/tests/declconfig/_expect.py new file mode 100644 index 00000000000..a8c6b2a9e92 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/_expect.py @@ -0,0 +1,704 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Matchers for a scenario's ``expect`` entries, checked against the received telemetry.""" + +from __future__ import annotations + +import operator +import re +from collections.abc import Callable, Iterable, Iterator, Sequence +from dataclasses import dataclass, field, replace +from typing import TYPE_CHECKING, Any, Generic, TypeVar + +import yaml + +from opentelemetry.proto.common.v1.common_pb2 import AnyValue, InstrumentationScope, KeyValue +from opentelemetry.proto.logs.v1.logs_pb2 import SeverityNumber +from opentelemetry.proto.metrics.v1.metrics_pb2 import AggregationTemporality, Metric +from opentelemetry.proto.trace.v1.trace_pb2 import Span, Status +from opentelemetry.test._otlp_test_server import RecordedLogRecord, RecordedMetric, RecordedSpan + +if TYPE_CHECKING: + from ._scenario import Received + +# Metric data types that each metric or data point field applies to. +FIELD_DATA_TYPES = { + "temporality": ("sum", "histogram", "exponential_histogram"), + "monotonic": ("sum",), + "value": ("gauge", "sum"), + "count": ("histogram", "exponential_histogram", "summary"), + "sum": ("histogram", "exponential_histogram", "summary"), + "min": ("histogram", "exponential_histogram"), + "max": ("histogram", "exponential_histogram"), + "explicit_bounds": ("histogram",), + "bucket_counts": ("histogram",), +} + +# Closest (or unexpected) candidates listed in a failure message. +_MAX_CANDIDATES = 3 + +_TYPED_KEYS = frozenset({"string", "bool", "int", "double", "kvlist"}) +_COMPARISONS: dict[str, Callable[[Any, Any], bool]] = { + "gt": operator.gt, + "gte": operator.ge, + "lt": operator.lt, + "lte": operator.le, +} + +# Received spans by ``(trace_id, span_id)``, for resolving parents, links and log correlation. +SpanIndex = dict[tuple[bytes, bytes], RecordedSpan] + +_T = TypeVar("_T") + + +def build_span_index(spans: Iterable[RecordedSpan]) -> SpanIndex: + return {(recorded.span.trace_id, recorded.span.span_id): recorded for recorded in spans} + + +def require_metric_field(metric: Metric, field_name: str) -> None: + data_type = metric.WhichOneof("data") + if data_type not in FIELD_DATA_TYPES[field_name]: + raise ValueError(f"Metric {metric.name!r} is a {data_type}, field {field_name!r} does not apply to it") + + +# Values are compared and described as plain Python values: str, bool, int, float, bytes, +# list for arrays and dict for kvlists. ``None`` stands for an absent value. +def _any_value_to_python(value: AnyValue) -> Any: + kind = value.WhichOneof("value") + if kind is None: + return None + if kind == "array_value": + return [_any_value_to_python(item) for item in value.array_value.values] + if kind == "kvlist_value": + return _key_values_to_python(value.kvlist_value.values) + return getattr(value, kind) + + +def _key_values_to_python(key_values: Iterable[KeyValue]) -> dict[str, Any]: + return {kv.key: _any_value_to_python(kv.value) for kv in key_values} + + +def _literal_to_python(literal: Any) -> Any: + """Resolve the typed literals of a scenario value, such as ``{double: 5}``, to plain values.""" + if isinstance(literal, dict): + ((kind, payload),) = literal.items() + if kind == "kvlist": + return {key: _literal_to_python(value) for key, value in payload.items()} + return float(payload) if kind == "double" else payload + if isinstance(literal, list): + return [_literal_to_python(item) for item in literal] + return literal + + +def _is_numeric_value(value: Any) -> bool: + # bool is a subclass of int, but not a number here. + return isinstance(value, (int, float)) and not isinstance(value, bool) + + +def _values_equal(expected: Any, actual: Any, strict: bool) -> bool: + """Compare values including their types, so ``5``, ``5.0``, ``"5"`` and ``True`` all differ. + + With ``strict=False``, numbers compare by value regardless of int or double. + """ + if not strict and _is_numeric_value(expected) and _is_numeric_value(actual): + return expected == actual + if type(expected) is not type(actual): + return False + if isinstance(expected, list): + return len(expected) == len(actual) and all( + _values_equal(item, other, strict) for item, other in zip(expected, actual) + ) + if isinstance(expected, dict): + return expected.keys() == actual.keys() and all( + _values_equal(value, actual[key], strict) for key, value in expected.items() + ) + return expected == actual + + +def _to_flow_yaml(value: Any) -> str: + dumped = yaml.safe_dump(value, default_flow_style=True, sort_keys=False, width=1000, allow_unicode=True) + # A plain scalar document ends with an explicit "..." marker. + return dumped.removesuffix("\n...\n").strip() + + +def _describe_value(value: Any) -> str: + return "" if value is None else _to_flow_yaml(value) + + +@dataclass(frozen=True) +class Mismatch: + """How one field of a received item fails its matcher.""" + + path: tuple[str, ...] + expected: str + actual: str + + def nested(self, prefix: str) -> Mismatch: + return replace(self, path=(prefix, *self.path)) + + def __str__(self) -> str: + # Index segments such as "['service.name']" attach without a dot. + path = "".join(segment if segment.startswith("[") else f".{segment}" for segment in self.path) + return f"{path.removeprefix('.')}: expected {self.expected}, got {self.actual}" + + +def _nest_mismatches(prefix: str, mismatches: Iterable[Mismatch]) -> Iterator[Mismatch]: + return (mismatch.nested(prefix) for mismatch in mismatches) + + +@dataclass(frozen=True) +class ValueMatcher: + """A literal (``equals``) or operators that must all hold, applied to one value. + + Without any of them, it matches anything, including an absent value. + """ + + equals: Any = None + pattern: re.Pattern[str] | None = None + exists: bool | None = None + bounds: tuple[tuple[str, float], ...] = () + + def describe(self) -> str: + if self.equals is not None: + return _describe_value(self.equals) + parts = [] + if self.exists is not None: + parts.append("present" if self.exists else "") + if self.pattern is not None: + parts.append(f"match for {self.pattern.pattern!r}") + parts.extend(f"{name} {bound}" for name, bound in self.bounds) + return " and ".join(parts) + + def check(self, label: str, actual: Any, strict: bool = True) -> Iterator[Mismatch]: + """Yield how ``actual`` fails this matcher, prefixed with ``label``. + + With ``strict=False``, numeric literals match int and double values alike. + """ + if self == _ANY_VALUE: + return + if (actual is None) != (self.exists is False) or not self._holds(actual, strict): + yield Mismatch((label,), self.describe(), _describe_value(actual)) + + def _holds(self, actual: Any, strict: bool) -> bool: + if actual is None: + return True + if self.equals is not None and not _values_equal(self.equals, actual, strict): + return False + if self.pattern is not None and (not isinstance(actual, str) or self.pattern.search(actual) is None): + return False + return all(_is_numeric_value(actual) and _COMPARISONS[name](actual, bound) for name, bound in self.bounds) + + +_ANY_VALUE = ValueMatcher() + + +def parse_value_matcher(spec: Any) -> ValueMatcher: + """Build a matcher from a ``value_matcher``, ``string_matcher`` or ``number_matcher``.""" + if not isinstance(spec, dict) or spec.keys() <= _TYPED_KEYS: + return ValueMatcher(equals=_literal_to_python(spec)) + pattern = spec.get("pattern") + return ValueMatcher( + pattern=re.compile(pattern) if pattern is not None else None, + exists=spec.get("exists"), + bounds=tuple((name, spec[name]) for name in _COMPARISONS if name in spec), + ) + + +@dataclass(frozen=True) +class LiteralMatcher(Generic[_T]): + """An exact, untagged value such as an enum name or a bool. Unset, it matches anything.""" + + expected: _T | None = None + + def check(self, label: str, actual: Any) -> Iterator[Mismatch]: + if self.expected is not None and self.expected != actual: + yield Mismatch((label,), str(self.expected), str(actual)) + + +@dataclass(frozen=True) +class AttributesMatcher: + matchers: dict[str, ValueMatcher] = field(default_factory=dict) + strict: bool = False + + def check(self, label: str, attributes: Sequence[KeyValue]) -> Iterator[Mismatch]: + actual = _key_values_to_python(attributes) + for key, matcher in self.matchers.items(): + yield from _nest_mismatches(label, matcher.check(f"[{key!r}]", actual.get(key))) + if self.strict and (unexpected := sorted(actual.keys() - self.matchers.keys())): + yield Mismatch((label,), "no unlisted keys", ", ".join(unexpected)) + + +@dataclass(frozen=True) +class ScopeMatcher: + name: ValueMatcher = ValueMatcher() + version: ValueMatcher = ValueMatcher() + attributes: AttributesMatcher = AttributesMatcher() + + def check(self, scope: InstrumentationScope) -> Iterator[Mismatch]: + yield from self.name.check("name", scope.name or None) + yield from self.version.check("version", scope.version or None) + yield from self.attributes.check("attributes", scope.attributes) + + +@dataclass(frozen=True) +class LinkMatcher: + trace_state: ValueMatcher = ValueMatcher() + span: SpanMatcher | None = None + attributes: AttributesMatcher = AttributesMatcher() + + def check(self, link: Span.Link, spans: SpanIndex) -> Iterator[Mismatch]: + yield from self.trace_state.check("trace_state", link.trace_state or None) + if self.span is not None: + yield from self.span.check_reference("span", spans, link.trace_id, link.span_id) + yield from self.attributes.check("attributes", link.attributes) + + +@dataclass(frozen=True) +class SpanMatcher: + name: ValueMatcher = ValueMatcher() + kind: LiteralMatcher[str] = LiteralMatcher() + status_code: LiteralMatcher[str] = LiteralMatcher() + status_message: ValueMatcher = ValueMatcher() + trace_state: ValueMatcher = ValueMatcher() + root: LiteralMatcher[bool] = LiteralMatcher() + parent: SpanMatcher | None = None + links: tuple[LinkMatcher, ...] = () + link_count: ValueMatcher = ValueMatcher() + scope: ScopeMatcher | None = None + attributes: AttributesMatcher = AttributesMatcher() + + def check(self, recorded: RecordedSpan, spans: SpanIndex) -> Iterator[Mismatch]: + span = recorded.span + yield from self.name.check("name", span.name or None) + yield from self.kind.check("kind", Span.SpanKind.Name(span.kind).removeprefix("SPAN_KIND_")) + status_code = Status.StatusCode.Name(span.status.code).removeprefix("STATUS_CODE_") + yield from _nest_mismatches("status", self.status_code.check("code", status_code)) + yield from _nest_mismatches("status", self.status_message.check("message", span.status.message or None)) + yield from self.trace_state.check("trace_state", span.trace_state or None) + yield from self.root.check("root", not span.parent_span_id) + if self.parent is not None: + yield from self.parent.check_reference("parent", spans, span.trace_id, span.parent_span_id) + for position, link_matcher in enumerate(self.links): + label = f"links[{position}]" + candidates = [list(link_matcher.check(link, spans)) for link in span.links] + if not candidates: + yield Mismatch((label,), "a link", "") + elif all(candidates): + yield from _nest_mismatches(label, min(candidates, key=len)) + yield from self.link_count.check("link_count", len(span.links), strict=False) + if self.scope is not None: + yield from _nest_mismatches("scope", self.scope.check(recorded.scope)) + yield from self.attributes.check("attributes", span.attributes) + + def check_reference(self, label: str, spans: SpanIndex, trace_id: bytes, span_id: bytes) -> Iterator[Mismatch]: + """Match the received span with the given IDs, yielding its mismatches nested under ``label``.""" + if not span_id: + yield Mismatch((label,), "a span", "") + return + target = spans.get((trace_id, span_id)) + if target is None: + yield Mismatch((label,), "a received span", span_id.hex()) + return + yield from _nest_mismatches(label, self.check(target, spans)) + + +def _get_optional_double(point: Any, field_name: str) -> float | None: + try: + present = point.HasField(field_name) + except ValueError: + # Not an optional field, so it is always present. + present = True + return getattr(point, field_name) if present else None + + +@dataclass(frozen=True) +class DataPointMatcher: + value: ValueMatcher | None = None + count: ValueMatcher | None = None + sum: ValueMatcher | None = None + min: ValueMatcher | None = None + max: ValueMatcher | None = None + explicit_bounds: LiteralMatcher[list[float]] | None = None + bucket_counts: LiteralMatcher[list[int]] | None = None + exemplar_count: ValueMatcher = ValueMatcher() + attributes: AttributesMatcher = AttributesMatcher() + + def check(self, point: Any, metric: Metric) -> Iterator[Mismatch]: + if self.value is not None: + require_metric_field(metric, "value") + kind = point.WhichOneof("value") + actual = getattr(point, kind) if kind else None + yield from self.value.check("value", actual) + if self.count is not None: + require_metric_field(metric, "count") + yield from self.count.check("count", point.count, strict=False) + for field_name in ("sum", "min", "max"): + if (matcher := getattr(self, field_name)) is not None: + require_metric_field(metric, field_name) + yield from matcher.check(field_name, _get_optional_double(point, field_name), strict=False) + for field_name in ("explicit_bounds", "bucket_counts"): + if (literal := getattr(self, field_name)) is not None: + require_metric_field(metric, field_name) + yield from literal.check(field_name, list(getattr(point, field_name))) + yield from self.exemplar_count.check("exemplar_count", len(point.exemplars), strict=False) + yield from self.attributes.check("attributes", point.attributes) + + +@dataclass(frozen=True) +class MetricMatcher: + name: ValueMatcher = ValueMatcher() + type: LiteralMatcher[str] = LiteralMatcher() + unit: ValueMatcher = ValueMatcher() + description: ValueMatcher = ValueMatcher() + temporality: LiteralMatcher[str] | None = None + monotonic: LiteralMatcher[bool] | None = None + scope: ScopeMatcher | None = None + data_point: DataPointMatcher | None = None + + def check(self, recorded: RecordedMetric, point: Any) -> Iterator[Mismatch]: + metric = recorded.metric + data_type = metric.WhichOneof("data") + identity = [*self.name.check("name", metric.name or None), *self.type.check("type", data_type)] + yield from identity + if identity: + # A different metric: skip the fields that may not apply to its type. + return + yield from self.unit.check("unit", metric.unit or None) + yield from self.description.check("description", metric.description or None) + if self.temporality is not None: + require_metric_field(metric, "temporality") + temporality = AggregationTemporality.Name(getattr(metric, data_type).aggregation_temporality) + yield from self.temporality.check("temporality", temporality.removeprefix("AGGREGATION_TEMPORALITY_")) + if self.monotonic is not None: + require_metric_field(metric, "monotonic") + yield from self.monotonic.check("monotonic", metric.sum.is_monotonic) + if self.scope is not None: + yield from _nest_mismatches("scope", self.scope.check(recorded.scope)) + if self.data_point is not None: + yield from _nest_mismatches("data_point", self.data_point.check(point, metric)) + + +@dataclass(frozen=True) +class LogMatcher: + body: ValueMatcher = ValueMatcher() + severity_number: LiteralMatcher[str] = LiteralMatcher() + severity_text: ValueMatcher = ValueMatcher() + event_name: ValueMatcher = ValueMatcher() + trace_context: LiteralMatcher[bool] = LiteralMatcher() + span: SpanMatcher | None = None + scope: ScopeMatcher | None = None + attributes: AttributesMatcher = AttributesMatcher() + + def check(self, recorded: RecordedLogRecord, spans: SpanIndex) -> Iterator[Mismatch]: + log_record = recorded.log_record + yield from self.body.check("body", _any_value_to_python(log_record.body)) + severity_number = SeverityNumber.Name(log_record.severity_number).removeprefix("SEVERITY_NUMBER_") + yield from self.severity_number.check("severity_number", severity_number) + yield from self.severity_text.check("severity_text", log_record.severity_text or None) + yield from self.event_name.check("event_name", log_record.event_name or None) + ids_set = (bool(log_record.trace_id), bool(log_record.span_id)) + yield from self.trace_context.check("trace_context", ids_set[0] if ids_set[0] == ids_set[1] else "partial") + if self.span is not None: + yield from self.span.check_reference("span", spans, log_record.trace_id, log_record.span_id) + if self.scope is not None: + yield from _nest_mismatches("scope", self.scope.check(recorded.scope)) + yield from self.attributes.check("attributes", log_record.attributes) + + +@dataclass(frozen=True) +class Counts: + count: int | None = None + min_count: int | None = None + max_count: int | None = None + + @property + def lower(self) -> int: + if self.count is not None: + return self.count + return self.min_count if self.min_count is not None else 1 + + @property + def upper(self) -> int | None: + return self.count if self.count is not None else self.max_count + + def satisfied(self, matched: int) -> bool: + return matched >= self.lower and (self.upper is None or matched <= self.upper) + + def __str__(self) -> str: + if self.count is not None: + return f"== {self.count}" + if self.upper is None: + return f">= {self.lower}" + return f">= {self.lower} and <= {self.upper}" + + +@dataclass(frozen=True) +class ExpectationFailure: + """An unmet ``expect`` entry: its source, a summary line, and indented detail lines.""" + + key: str + source: dict[str, Any] + summary: str + details: tuple[str, ...] = () + + def __str__(self) -> str: + header = _to_flow_yaml(self.source) + return "\n".join([f"{self.key}: {header}", f" {self.summary}", *(f" {detail}" for detail in self.details)]) + + +def _count_failure( + key: str, source: dict[str, Any], counts: Counts, candidates: list[tuple[str, list[Mismatch]]], noun: str +) -> ExpectationFailure | None: + """Report a counted expectation, given ``(label, mismatches)`` for every candidate item.""" + matched = [label for label, mismatches in candidates if not mismatches] + if counts.satisfied(len(matched)): + return None + details: list[str] = [] + if not candidates: + details.append(f"no {noun} received") + elif len(matched) < counts.lower: + unmatched = dict.fromkeys((label, tuple(mismatches)) for label, mismatches in candidates if mismatches) + # Prefer candidates with the expected name, then those with the fewest mismatches. + closest = sorted( + unmatched, + key=lambda item: (any(mismatch.path == ("name",) for mismatch in item[1]), len(item[1])), + ) + details.extend( + f"closest {label}: {'; '.join(map(str, mismatches))}" for label, mismatches in closest[:_MAX_CANDIDATES] + ) + else: + details.extend(f"matched {label}" for label in matched[:_MAX_CANDIDATES]) + return ExpectationFailure(key, source, f"matched {len(matched)}, expected {counts}", tuple(details)) + + +@dataclass(frozen=True) +class SpansExpectation: + matcher: SpanMatcher + counts: Counts = Counts() + source: dict[str, Any] = field(default_factory=dict, compare=False, repr=False) + + def failure(self, received: Received, spans: SpanIndex) -> ExpectationFailure | None: + candidates = [ + (repr(recorded.span.name), list(self.matcher.check(recorded, spans))) for recorded in received.spans + ] + return _count_failure("spans", self.source, self.counts, candidates, "spans") + + +def _get_data_points(metric: Metric) -> Sequence[Any]: + data_type = metric.WhichOneof("data") + return getattr(metric, data_type).data_points if data_type is not None else () + + +@dataclass(frozen=True) +class MetricExpectation: + matcher: MetricMatcher + counts: Counts = Counts() + source: dict[str, Any] = field(default_factory=dict, compare=False, repr=False) + + def failure(self, received: Received, spans: SpanIndex) -> ExpectationFailure | None: + candidates = [ + ( + f"{recorded.metric.name!r} {_describe_value(_key_values_to_python(point.attributes))}", + list(self.matcher.check(recorded, point)), + ) + for recorded in received.metrics + for point in _get_data_points(recorded.metric) + ] + return _count_failure("metric", self.source, self.counts, candidates, "metric data points") + + +@dataclass(frozen=True) +class LogsExpectation: + matcher: LogMatcher + counts: Counts = Counts() + source: dict[str, Any] = field(default_factory=dict, compare=False, repr=False) + + def failure(self, received: Received, spans: SpanIndex) -> ExpectationFailure | None: + candidates = [ + ( + f"body={_describe_value(_any_value_to_python(recorded.log_record.body))}", + list(self.matcher.check(recorded, spans)), + ) + for recorded in received.logs + ] + return _count_failure("logs", self.source, self.counts, candidates, "logs") + + +@dataclass(frozen=True) +class ResourceExpectation: + attributes: AttributesMatcher = AttributesMatcher() + schema_url: ValueMatcher = ValueMatcher() + source: dict[str, Any] = field(default_factory=dict, compare=False, repr=False) + + def failure(self, received: Received, spans: SpanIndex) -> ExpectationFailure | None: + resources = [ + *(("span", recorded.resource, recorded.resource_schema_url) for recorded in received.spans), + *(("metric", recorded.resource, recorded.resource_schema_url) for recorded in received.metrics), + *(("log", recorded.resource, recorded.resource_schema_url) for recorded in received.logs), + ] + if not resources: + return ExpectationFailure("resource", self.source, "nothing received") + failing = 0 + # One example per distinct non-matching resource. + examples: dict[tuple[bytes, str], tuple[str, list[Mismatch]]] = {} + for signal, resource, schema_url in resources: + mismatches = [ + *self.schema_url.check("schema_url", schema_url or None), + *self.attributes.check("attributes", resource.attributes), + ] + if mismatches: + failing += 1 + examples.setdefault((resource.SerializeToString(deterministic=True), schema_url), (signal, mismatches)) + if not failing: + return None + return ExpectationFailure( + "resource", + self.source, + f"{failing} of {len(resources)} items have a non-matching resource", + tuple( + f"{signal}: {'; '.join(map(str, mismatches))}" + for signal, mismatches in list(examples.values())[:_MAX_CANDIDATES] + ), + ) + + +@dataclass(frozen=True) +class OutputExpectation: + """A regex that must (or, with ``exists=False``, must not) be found in the app's stdout or stderr.""" + + stream: str + pattern: re.Pattern[str] + exists: bool = True + source: dict[str, Any] = field(default_factory=dict, compare=False, repr=False) + + def failure(self, received: Received, spans: SpanIndex) -> ExpectationFailure | None: + found = self.pattern.search(getattr(received, self.stream)) is not None + if found == self.exists: + return None + summary = f"{self.stream} {'has no' if self.exists else 'has a'} match for {self.pattern.pattern!r}" + return ExpectationFailure("output", self.source, summary) + + +Expectation = SpansExpectation | MetricExpectation | LogsExpectation | ResourceExpectation | OutputExpectation + + +def _parse_value(spec: dict[str, Any], key: str) -> ValueMatcher: + return parse_value_matcher(spec[key]) if key in spec else ValueMatcher() + + +def _parse_optional(spec: dict[str, Any], key: str) -> ValueMatcher | None: + return parse_value_matcher(spec[key]) if key in spec else None + + +def _parse_optional_literal(spec: dict[str, Any], key: str) -> LiteralMatcher[Any] | None: + return LiteralMatcher(spec[key]) if key in spec else None + + +def _parse_attributes(spec: dict[str, Any]) -> AttributesMatcher: + if (attributes := spec.get("attributes")) is None: + return AttributesMatcher() + return AttributesMatcher( + matchers={key: parse_value_matcher(value) for key, value in attributes.get("values", {}).items()}, + strict=attributes.get("strict", False), + ) + + +def _parse_scope(spec: dict[str, Any]) -> ScopeMatcher | None: + if "scope" not in spec: + return None + scope = spec["scope"] + return ScopeMatcher( + name=_parse_value(scope, "name"), + version=_parse_value(scope, "version"), + attributes=_parse_attributes(scope), + ) + + +def _parse_span_matcher(spec: dict[str, Any]) -> SpanMatcher: + status = spec.get("status", {}) + return SpanMatcher( + name=_parse_value(spec, "name"), + kind=LiteralMatcher(spec.get("kind")), + status_code=LiteralMatcher(status.get("code")), + status_message=_parse_value(status, "message"), + trace_state=_parse_value(spec, "trace_state"), + root=LiteralMatcher(spec.get("root")), + parent=_parse_span_matcher(spec["parent"]) if "parent" in spec else None, + links=tuple( + LinkMatcher( + trace_state=_parse_value(link, "trace_state"), + span=_parse_span_matcher(link["span"]) if "span" in link else None, + attributes=_parse_attributes(link), + ) + for link in spec.get("links", []) + ), + link_count=_parse_value(spec, "link_count"), + scope=_parse_scope(spec), + attributes=_parse_attributes(spec), + ) + + +def _parse_data_point_matcher(spec: dict[str, Any]) -> DataPointMatcher: + return DataPointMatcher( + value=_parse_optional(spec, "value"), + count=_parse_optional(spec, "count"), + sum=_parse_optional(spec, "sum"), + min=_parse_optional(spec, "min"), + max=_parse_optional(spec, "max"), + explicit_bounds=_parse_optional_literal(spec, "explicit_bounds"), + bucket_counts=_parse_optional_literal(spec, "bucket_counts"), + exemplar_count=_parse_value(spec, "exemplar_count"), + attributes=_parse_attributes(spec), + ) + + +def _parse_metric_matcher(spec: dict[str, Any]) -> MetricMatcher: + return MetricMatcher( + name=_parse_value(spec, "name"), + type=LiteralMatcher(spec.get("type")), + unit=_parse_value(spec, "unit"), + description=_parse_value(spec, "description"), + temporality=_parse_optional_literal(spec, "temporality"), + monotonic=_parse_optional_literal(spec, "monotonic"), + scope=_parse_scope(spec), + data_point=_parse_data_point_matcher(spec["data_point"]) if "data_point" in spec else None, + ) + + +def _parse_log_matcher(spec: dict[str, Any]) -> LogMatcher: + return LogMatcher( + body=_parse_value(spec, "body"), + severity_number=LiteralMatcher(spec.get("severity_number")), + severity_text=_parse_value(spec, "severity_text"), + event_name=_parse_value(spec, "event_name"), + trace_context=LiteralMatcher(spec.get("trace_context")), + span=_parse_span_matcher(spec["span"]) if "span" in spec else None, + scope=_parse_scope(spec), + attributes=_parse_attributes(spec), + ) + + +def _parse_counts(spec: dict[str, Any]) -> Counts: + return Counts(count=spec.get("count"), min_count=spec.get("min_count"), max_count=spec.get("max_count")) + + +def parse_expectation(item: dict[str, Any]) -> Expectation: + """Build an expectation from one schema-validated ``expect`` item.""" + if (spans := item.get("spans")) is not None: + return SpansExpectation(matcher=_parse_span_matcher(spans), counts=_parse_counts(spans), source=spans) + if (metric := item.get("metric")) is not None: + return MetricExpectation(matcher=_parse_metric_matcher(metric), counts=_parse_counts(metric), source=metric) + if (logs := item.get("logs")) is not None: + return LogsExpectation(matcher=_parse_log_matcher(logs), counts=_parse_counts(logs), source=logs) + if (output := item.get("output")) is not None: + return OutputExpectation( + stream=output["stream"], + pattern=re.compile(output["pattern"]), + exists=output.get("exists", True), + source=output, + ) + resource = item["resource"] + return ResourceExpectation( + attributes=_parse_attributes(resource), schema_url=_parse_value(resource, "schema_url"), source=resource + ) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/_scenario.py b/tests/opentelemetry-docker-tests/tests/declconfig/_scenario.py new file mode 100644 index 00000000000..383fb2f9711 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/_scenario.py @@ -0,0 +1,187 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +"""Loading and evaluation of a scenario's ``scenario.yaml``.""" + +from __future__ import annotations + +import json +import re +from dataclasses import dataclass, field +from functools import cache +from pathlib import Path +from typing import Any + +import jsonschema +import yaml + +from opentelemetry.proto.metrics.v1.metrics_pb2 import Metric +from opentelemetry.test._otlp_test_server import ( + OtlpProtoTestServer, + RecordedLogRecord, + RecordedMetric, + RecordedSpan, +) + +from ._expect import Expectation, ExpectationFailure, build_span_index, parse_expectation, require_metric_field + +_SCHEMA_FILE = Path(__file__).parent / "scenario.schema.json" +_DEFAULT_TIMEOUT = 30.0 + + +@dataclass +class Received: + """Telemetry accumulated from the sink over a scenario run, along with the app's state.""" + + spans: list[RecordedSpan] = field(default_factory=list) + metrics: list[RecordedMetric] = field(default_factory=list) + logs: list[RecordedLogRecord] = field(default_factory=list) + # Whether the app has exited cleanly on its own. + exited: bool = False + # The app's output, captured once it has stopped. + stdout: str = "" + stderr: str = "" + + def drain(self, sink: OtlpProtoTestServer) -> int: + """Collect what the sink received since the last drain and return how many items that was.""" + spans = sink.drain_spans() + metrics = sink.drain_metrics() + logs = sink.drain_log_records() + self.spans.extend(spans) + self.metrics.extend(metrics) + self.logs.extend(logs) + return len(spans) + len(metrics) + len(logs) + + +@dataclass(frozen=True) +class SpansCondition: + min_count: int = 1 + name: str | None = None + + def _get_count(self, received: Received) -> int: + return sum(1 for recorded in received.spans if self.name is None or recorded.span.name == self.name) + + def satisfied(self, received: Received) -> bool: + return self._get_count(received) >= self.min_count + + def progress(self, received: Received) -> str: + return f"spans(name={self.name!r}): {self._get_count(received)}/{self.min_count}" + + +@dataclass(frozen=True) +class MetricCondition: + name: str + min: float + field: str = "value" + + def _get_field_values(self, metric: Metric) -> list[float]: + require_metric_field(metric, self.field) + data_points = getattr(metric, metric.WhichOneof("data")).data_points + if self.field == "value": + return [getattr(point, point.WhichOneof("value")) for point in data_points if point.WhichOneof("value")] + return [getattr(point, self.field) for point in data_points] + + def _get_max(self, received: Received) -> float | None: + values = ( + value + for recorded in received.metrics + if recorded.metric.name == self.name + for value in self._get_field_values(recorded.metric) + ) + return max(values, default=None) + + def satisfied(self, received: Received) -> bool: + highest = self._get_max(received) + return highest is not None and highest >= self.min + + def progress(self, received: Received) -> str: + return f"metric(name={self.name!r}, field={self.field!r}): max {self._get_max(received)} (need >= {self.min})" + + +@dataclass(frozen=True) +class LogsCondition: + min_count: int = 1 + body_pattern: re.Pattern[str] | None = None + + def _matches(self, recorded: RecordedLogRecord) -> bool: + if self.body_pattern is None: + return True + body = recorded.log_record.body + return body.WhichOneof("value") == "string_value" and self.body_pattern.search(body.string_value) is not None + + def _get_count(self, received: Received) -> int: + return sum(1 for recorded in received.logs if self._matches(recorded)) + + def satisfied(self, received: Received) -> bool: + return self._get_count(received) >= self.min_count + + def progress(self, received: Received) -> str: + pattern = self.body_pattern.pattern if self.body_pattern is not None else None + return f"logs(body_pattern={pattern!r}): {self._get_count(received)}/{self.min_count}" + + +@dataclass(frozen=True) +class ExitedCondition: + def satisfied(self, received: Received) -> bool: + return received.exited + + def progress(self, received: Received) -> str: + return f"exited: {received.exited}" + + +Condition = SpansCondition | MetricCondition | LogsCondition | ExitedCondition + + +@dataclass(frozen=True) +class Scenario: + description: str + timeout: float + until: list[Condition] + expect: list[Expectation] = field(default_factory=list) + env: dict[str, str] = field(default_factory=dict) + + def unmet(self, received: Received) -> list[Condition]: + return [condition for condition in self.until if not condition.satisfied(received)] + + def failed_expectations(self, received: Received) -> list[ExpectationFailure]: + """Return a failure for each ``expect`` entry the received telemetry doesn't satisfy.""" + spans = build_span_index(received.spans) + return [failure for expectation in self.expect if (failure := expectation.failure(received, spans)) is not None] + + +@cache +def _get_schema() -> dict[str, Any]: + with open(_SCHEMA_FILE, encoding="utf-8") as schema_file: + return json.load(schema_file) + + +def _parse_condition(item: dict[str, Any]) -> Condition: + if (spans := item.get("spans")) is not None: + return SpansCondition(**spans) + if (metric := item.get("metric")) is not None: + return MetricCondition(**metric) + if "exited" in item: + return ExitedCondition() + logs = item["logs"] + pattern = logs.get("body_pattern") + return LogsCondition( + min_count=logs.get("min_count", 1), + body_pattern=re.compile(pattern) if pattern is not None else None, + ) + + +def parse_scenario(document: Any) -> Scenario: + """Validate a loaded ``scenario.yaml`` document and build a :class:`Scenario`.""" + jsonschema.validate(document, _get_schema()) + return Scenario( + description=document["description"], + timeout=float(document.get("timeout", _DEFAULT_TIMEOUT)), + until=[_parse_condition(item) for item in document["until"]], + expect=[parse_expectation(item) for item in document.get("expect", [])], + env=document.get("env", {}), + ) + + +def load_scenario(path: Path) -> Scenario: + with open(path, encoding="utf-8") as scenario_file: + return parse_scenario(yaml.safe_load(scenario_file)) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/collector-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/collector-config.yaml new file mode 100644 index 00000000000..2c3a74ddf16 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/collector-config.yaml @@ -0,0 +1,29 @@ +# Default collector config, used by scenarios without their own collector-config.yaml. +extensions: + health_check: + endpoint: 0.0.0.0:13133 + +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +exporters: + otlphttp: + endpoint: ${env:OTLP_SINK_ENDPOINT} + +service: + extensions: [health_check] + pipelines: + traces: + receivers: [otlp] + exporters: [otlphttp] + metrics: + receivers: [otlp] + exporters: [otlphttp] + logs: + receivers: [otlp] + exporters: [otlphttp] diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/conftest.py b/tests/opentelemetry-docker-tests/tests/declconfig/conftest.py new file mode 100644 index 00000000000..0e72943bf03 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/conftest.py @@ -0,0 +1,84 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import re +from collections.abc import Iterator +from pathlib import Path +from typing import Any + +import pytest +import yaml +from testcontainers.core.container import DockerContainer +from testcontainers.core.wait_strategies import HttpWaitStrategy + +from opentelemetry.test._otlp_test_server import OtlpProtoTestServer + +collect_ignore_glob = ["scenarios/*"] + +_DOCKER_COMPOSE_FILE = Path(__file__).parent.parent / "docker-compose.yml" +_DEFAULT_COLLECTOR_CONFIG = Path(__file__).parent / "collector-config.yaml" +_COLLECTOR_CONFIG_PATH = "/etc/otelcol/collector-config.yaml" +_HEALTH_CHECK_PORT = 13133 +_ENDPOINT_PORT_RE = re.compile(r":(\d+)$") + + +def _get_collector_image() -> str: + """Reuse the collector image pinned in docker-compose.yml.""" + with open(_DOCKER_COMPOSE_FILE, encoding="utf-8") as compose_file: + return yaml.safe_load(compose_file)["services"]["otcollector"]["image"] + + +def _get_receiver_ports(collector_config: Path) -> list[int]: + """Collect the ``host:port`` receiver endpoints declared in a collector config.""" + with open(collector_config, encoding="utf-8") as config_file: + receivers = yaml.safe_load(config_file).get("receivers") or {} + + ports: set[int] = set() + + def _walk(node: Any) -> None: + if isinstance(node, dict): + for key, value in node.items(): + if key == "endpoint" and isinstance(value, str) and (match := _ENDPOINT_PORT_RE.search(value)): + ports.add(int(match.group(1))) + else: + _walk(value) + elif isinstance(node, list): + for item in node: + _walk(item) + + _walk(receivers) + return sorted(ports) + + +@pytest.fixture +def otlp_sink() -> Iterator[OtlpProtoTestServer]: + """Host side OTLP receiver the scenario's collector forwards to.""" + with OtlpProtoTestServer(host="0.0.0.0", port=0) as sink: + yield sink + + +@pytest.fixture +def collector(scenario: Path, otlp_sink: OtlpProtoTestServer) -> Iterator[str]: + """Start a collector with the scenario's config, or the default one, and yield its host. + + Receiver ports are bound 1:1 on the host, so scenario configs reach them + as ``://${OTEL_COLLECTOR_ENDPOINT}:``. + """ + collector_config = scenario / "collector-config.yaml" + if not collector_config.exists(): + collector_config = _DEFAULT_COLLECTOR_CONFIG + container = ( + DockerContainer(_get_collector_image()) + .with_volume_mapping(collector_config, _COLLECTOR_CONFIG_PATH) + .with_command(f"--config={_COLLECTOR_CONFIG_PATH}") + .with_env("OTLP_SINK_ENDPOINT", f"http://host.docker.internal:{otlp_sink.port}") + .with_kwargs(extra_hosts={"host.docker.internal": "host-gateway"}) + .with_exposed_ports(_HEALTH_CHECK_PORT) + .waiting_for(HttpWaitStrategy(_HEALTH_CHECK_PORT).for_status_code(200)) + ) + for port in _get_receiver_ports(collector_config): + container.with_bind_ports(port, port) + with container: + yield container.get_container_host_ip() diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenario.schema.json b/tests/opentelemetry-docker-tests/tests/declconfig/scenario.schema.json new file mode 100644 index 00000000000..57a5f0336de --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenario.schema.json @@ -0,0 +1,457 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Declarative config docker-test scenario", + "type": "object", + "additionalProperties": false, + "required": ["description", "until"], + "properties": { + "description": { + "type": "string", + "minLength": 1 + }, + "timeout": { + "description": "Seconds to wait for every `until` condition to hold.", + "type": "number", + "exclusiveMinimum": 0, + "default": 30 + }, + "env": { + "description": "Extra environment variables for the app.", + "type": "object", + "additionalProperties": {"type": "string"} + }, + "until": { + "description": "Termination conditions; all of them must hold.", + "type": "array", + "minItems": 1, + "items": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["spans"], + "properties": {"spans": {"$ref": "#/$defs/spans"}} + }, + { + "type": "object", + "additionalProperties": false, + "required": ["metric"], + "properties": {"metric": {"$ref": "#/$defs/metric"}} + }, + { + "type": "object", + "additionalProperties": false, + "required": ["logs"], + "properties": {"logs": {"$ref": "#/$defs/logs"}} + }, + { + "type": "object", + "additionalProperties": false, + "required": ["exited"], + "properties": { + "exited": { + "description": "Met once the app has exited cleanly.", + "type": "object", + "additionalProperties": false + } + } + } + ] + } + }, + "expect": { + "description": "Content assertions, checked after the app has stopped and the sink is drained.", + "type": "array", + "items": { + "oneOf": [ + { + "type": "object", + "additionalProperties": false, + "required": ["spans"], + "properties": {"spans": {"$ref": "#/$defs/spans_expectation"}} + }, + { + "type": "object", + "additionalProperties": false, + "required": ["metric"], + "properties": {"metric": {"$ref": "#/$defs/metric_expectation"}} + }, + { + "type": "object", + "additionalProperties": false, + "required": ["logs"], + "properties": {"logs": {"$ref": "#/$defs/logs_expectation"}} + }, + { + "type": "object", + "additionalProperties": false, + "required": ["resource"], + "properties": {"resource": {"$ref": "#/$defs/resource_matcher"}} + }, + { + "type": "object", + "additionalProperties": false, + "required": ["output"], + "properties": {"output": {"$ref": "#/$defs/output_expectation"}} + } + ] + } + } + }, + "$defs": { + "min_count": { + "type": "integer", + "minimum": 1, + "default": 1 + }, + "spans": { + "type": "object", + "additionalProperties": false, + "properties": { + "min_count": {"$ref": "#/$defs/min_count"}, + "name": { + "description": "Exact match on Span.name.", + "type": "string", + "minLength": 1 + } + } + }, + "metric": { + "type": "object", + "additionalProperties": false, + "required": ["name", "min"], + "properties": { + "name": { + "description": "Exact match on Metric.name.", + "type": "string", + "minLength": 1 + }, + "field": { + "description": "`value` for Gauge/Sum; `count` or `sum` for Histogram/ExponentialHistogram/Summary.", + "enum": ["value", "count", "sum"], + "default": "value" + }, + "min": { + "description": "Met once any received data point has field >= min.", + "type": "number" + } + } + }, + "logs": { + "type": "object", + "additionalProperties": false, + "properties": { + "min_count": {"$ref": "#/$defs/min_count"}, + "body_pattern": { + "description": "Regex searched (re.search) in string log bodies.", + "type": "string", + "minLength": 1 + } + } + }, + "typed_literal": { + "description": "A literal of an explicit AnyValue type.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "maxProperties": 1, + "properties": { + "string": {"type": "string"}, + "bool": {"type": "boolean"}, + "int": {"type": "integer"}, + "double": {"type": "number"}, + "kvlist": {"type": "object", "additionalProperties": {"$ref": "#/$defs/value_literal"}} + } + }, + "value_literal": { + "description": "Type-strict literal: string, bool, int (integer), double (float), array (list) or a typed_literal.", + "oneOf": [ + {"type": ["string", "number", "boolean"]}, + {"type": "array", "items": {"$ref": "#/$defs/value_literal"}}, + {"$ref": "#/$defs/typed_literal"} + ] + }, + "value_operators": { + "description": "Operators, all of which must hold. `exists: false` stands alone.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "pattern": { + "description": "Regex searched (re.search) in a string value.", + "type": "string", + "minLength": 1 + }, + "exists": {"type": "boolean"}, + "gt": {"type": "number"}, + "gte": {"type": "number"}, + "lt": {"type": "number"}, + "lte": {"type": "number"} + }, + "if": {"required": ["exists"], "properties": {"exists": {"const": false}}}, + "then": {"maxProperties": 1} + }, + "value_matcher": { + "oneOf": [{"$ref": "#/$defs/value_literal"}, {"$ref": "#/$defs/value_operators"}] + }, + "string_matcher": { + "description": "Exact string, or `pattern`/`exists` operators. An empty string field counts as absent.", + "oneOf": [ + {"type": "string"}, + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "pattern": {"type": "string", "minLength": 1}, + "exists": {"type": "boolean"} + }, + "if": {"required": ["exists"], "properties": {"exists": {"const": false}}}, + "then": {"maxProperties": 1} + } + ] + }, + "number_matcher": { + "description": "Number, or `exists`/`gt`/`gte`/`lt`/`lte` operators.", + "oneOf": [ + {"type": "number"}, + { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "exists": {"type": "boolean"}, + "gt": {"type": "number"}, + "gte": {"type": "number"}, + "lt": {"type": "number"}, + "lte": {"type": "number"} + }, + "if": {"required": ["exists"], "properties": {"exists": {"const": false}}}, + "then": {"maxProperties": 1} + } + ] + }, + "attributes": { + "description": "Attribute assertions. Unlisted attributes are ignored unless `strict` is set.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "values": { + "description": "Attributes that must match.", + "type": "object", + "additionalProperties": {"$ref": "#/$defs/value_matcher"} + }, + "strict": { + "description": "Fail on attributes not listed in `values`.", + "type": "boolean", + "default": false + } + } + }, + "counts": { + "description": "How many items must match. `min_count` defaults to 1 unless `count` is set.", + "properties": { + "count": {"type": "integer", "minimum": 0}, + "min_count": {"type": "integer", "minimum": 1}, + "max_count": {"type": "integer", "minimum": 1} + }, + "not": {"anyOf": [{"required": ["count", "min_count"]}, {"required": ["count", "max_count"]}]} + }, + "resource_matcher": { + "description": "Every received span, metric and log must carry a matching resource.", + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "schema_url": {"$ref": "#/$defs/string_matcher"}, + "attributes": {"$ref": "#/$defs/attributes"} + } + }, + "output_expectation": { + "description": "A regex searched in the app's captured stdout or stderr.", + "type": "object", + "additionalProperties": false, + "required": ["stream", "pattern"], + "properties": { + "stream": {"enum": ["stdout", "stderr"]}, + "pattern": {"type": "string", "minLength": 1}, + "exists": { + "description": "Whether the pattern must be found (true) or must not be found (false).", + "type": "boolean", + "default": true + } + } + }, + "scope_matcher": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": {"$ref": "#/$defs/string_matcher"}, + "version": {"$ref": "#/$defs/string_matcher"}, + "attributes": {"$ref": "#/$defs/attributes"} + } + }, + "span_fields": { + "properties": { + "name": {"$ref": "#/$defs/string_matcher"}, + "kind": {"enum": ["UNSPECIFIED", "INTERNAL", "SERVER", "CLIENT", "PRODUCER", "CONSUMER"]}, + "status": { + "type": "object", + "additionalProperties": false, + "minProperties": 1, + "properties": { + "code": {"enum": ["UNSET", "OK", "ERROR"]}, + "message": {"$ref": "#/$defs/string_matcher"} + } + }, + "trace_state": {"$ref": "#/$defs/string_matcher"}, + "root": {"description": "Whether parent_span_id is empty.", "type": "boolean"}, + "parent": { + "description": "Matcher for the received span that is this span's parent.", + "$ref": "#/$defs/span_matcher" + }, + "links": { + "description": "Each entry must be matched by some link of the span.", + "type": "array", + "items": {"$ref": "#/$defs/link_matcher"} + }, + "link_count": {"description": "Number of links on the span.", "$ref": "#/$defs/number_matcher"}, + "scope": {"$ref": "#/$defs/scope_matcher"}, + "attributes": {"$ref": "#/$defs/attributes"} + }, + "not": {"required": ["root", "parent"]} + }, + "span_matcher": { + "type": "object", + "$ref": "#/$defs/span_fields", + "unevaluatedProperties": false + }, + "spans_expectation": { + "type": "object", + "allOf": [{"$ref": "#/$defs/span_fields"}, {"$ref": "#/$defs/counts"}], + "unevaluatedProperties": false + }, + "link_matcher": { + "type": "object", + "additionalProperties": false, + "properties": { + "trace_state": {"$ref": "#/$defs/string_matcher"}, + "span": { + "description": "Matcher for the received span the link points to.", + "$ref": "#/$defs/span_matcher" + }, + "attributes": {"$ref": "#/$defs/attributes"} + } + }, + "data_point_matcher": { + "description": "Fields that don't apply to the received metric's type fail the test.", + "type": "object", + "additionalProperties": false, + "properties": { + "value": { + "description": "Gauge/Sum. Type-strict: an integer matches as_int, a float as_double.", + "$ref": "#/$defs/number_matcher" + }, + "count": { + "description": "Histogram/ExponentialHistogram/Summary.", + "$ref": "#/$defs/number_matcher" + }, + "sum": { + "description": "Histogram/ExponentialHistogram/Summary.", + "$ref": "#/$defs/number_matcher" + }, + "min": {"description": "Histogram/ExponentialHistogram.", "$ref": "#/$defs/number_matcher"}, + "max": {"description": "Histogram/ExponentialHistogram.", "$ref": "#/$defs/number_matcher"}, + "explicit_bounds": { + "description": "Histogram, exact list.", + "type": "array", + "items": {"type": "number"} + }, + "bucket_counts": { + "description": "Histogram, exact list.", + "type": "array", + "items": {"type": "integer", "minimum": 0} + }, + "exemplar_count": {"description": "Number of exemplars on the data point.", "$ref": "#/$defs/number_matcher"}, + "attributes": {"$ref": "#/$defs/attributes"} + } + }, + "metric_expectation": { + "description": "Matched against every (metric, data point) pair across all exports.", + "type": "object", + "allOf": [ + { + "properties": { + "name": {"$ref": "#/$defs/string_matcher"}, + "type": {"enum": ["gauge", "sum", "histogram", "exponential_histogram", "summary"]}, + "unit": {"$ref": "#/$defs/string_matcher"}, + "description": {"$ref": "#/$defs/string_matcher"}, + "temporality": { + "description": "Sum/Histogram/ExponentialHistogram.", + "enum": ["DELTA", "CUMULATIVE"] + }, + "monotonic": {"description": "Sum.", "type": "boolean"}, + "scope": {"$ref": "#/$defs/scope_matcher"}, + "data_point": {"$ref": "#/$defs/data_point_matcher"} + } + }, + {"$ref": "#/$defs/counts"} + ], + "unevaluatedProperties": false + }, + "logs_expectation": { + "type": "object", + "allOf": [ + { + "properties": { + "body": {"$ref": "#/$defs/value_matcher"}, + "severity_number": { + "enum": [ + "UNSPECIFIED", + "TRACE", + "TRACE2", + "TRACE3", + "TRACE4", + "DEBUG", + "DEBUG2", + "DEBUG3", + "DEBUG4", + "INFO", + "INFO2", + "INFO3", + "INFO4", + "WARN", + "WARN2", + "WARN3", + "WARN4", + "ERROR", + "ERROR2", + "ERROR3", + "ERROR4", + "FATAL", + "FATAL2", + "FATAL3", + "FATAL4" + ] + }, + "severity_text": {"$ref": "#/$defs/string_matcher"}, + "event_name": {"$ref": "#/$defs/string_matcher"}, + "trace_context": { + "description": "Whether trace_id and span_id are both set (true) or both empty (false).", + "type": "boolean" + }, + "span": { + "description": "Matcher for the received span the record was emitted in.", + "$ref": "#/$defs/span_matcher" + }, + "scope": {"$ref": "#/$defs/scope_matcher"}, + "attributes": {"$ref": "#/$defs/attributes"} + } + }, + {"$ref": "#/$defs/counts"} + ], + "unevaluatedProperties": false + } + } +} diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_grpc_deflate_compression/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_grpc_deflate_compression/otel-config.yaml new file mode 100644 index 00000000000..9cfd8a0ae7b --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_grpc_deflate_compression/otel-config.yaml @@ -0,0 +1,12 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-error-grpc-deflate-compression +tracer_provider: + processors: + - simple: + exporter: + otlp_grpc: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4317 + compression: deflate diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_grpc_deflate_compression/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_grpc_deflate_compression/run.py new file mode 100644 index 00000000000..26611934cd9 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_grpc_deflate_compression/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("error-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_grpc_deflate_compression/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_grpc_deflate_compression/scenario.yaml new file mode 100644 index 00000000000..b78a0bcc946 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_grpc_deflate_compression/scenario.yaml @@ -0,0 +1,13 @@ +description: > + `deflate` compression is only supported over HTTP, so it fails for `otlp_grpc`. +until: + - exited: {} +expect: + - output: + stream: stderr + pattern: Failed to auto initialize OpenTelemetry + - output: + stream: stderr + pattern: "deflate" + - spans: + count: 0 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_schema_violation/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_schema_violation/otel-config.yaml new file mode 100644 index 00000000000..9d42f21bff7 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_schema_violation/otel-config.yaml @@ -0,0 +1,12 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-error-schema-violation +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces + timeout: not-a-number diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_schema_violation/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_schema_violation/run.py new file mode 100644 index 00000000000..26611934cd9 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_schema_violation/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("error-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_schema_violation/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_schema_violation/scenario.yaml new file mode 100644 index 00000000000..d5cf7c0a713 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_schema_violation/scenario.yaml @@ -0,0 +1,13 @@ +description: > + A config that doesn't match the schema fails configuration. +until: + - exited: {} +expect: + - output: + stream: stderr + pattern: Failed to auto initialize OpenTelemetry + - output: + stream: stderr + pattern: "Configuration does not match schema" + - spans: + count: 0 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unknown_propagator/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unknown_propagator/otel-config.yaml new file mode 100644 index 00000000000..d802fd9e896 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unknown_propagator/otel-config.yaml @@ -0,0 +1,13 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-error-unknown-propagator +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces +propagator: + composite_list: tracecontext,bogus diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unknown_propagator/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unknown_propagator/run.py new file mode 100644 index 00000000000..26611934cd9 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unknown_propagator/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("error-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unknown_propagator/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unknown_propagator/scenario.yaml new file mode 100644 index 00000000000..bfdbd44fd94 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unknown_propagator/scenario.yaml @@ -0,0 +1,11 @@ +description: > + An unknown propagator name in `composite_list` fails configuration. +until: + - exited: {} +expect: + - output: + stream: stderr + pattern: Failed to auto initialize OpenTelemetry + - output: + stream: stderr + pattern: "bogus" diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_file_format/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_file_format/otel-config.yaml new file mode 100644 index 00000000000..6255a74503a --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_file_format/otel-config.yaml @@ -0,0 +1,11 @@ +file_format: "2.0" +resource: + attributes: + - name: service.name + value: declconfig-error-unsupported-file-format +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_file_format/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_file_format/run.py new file mode 100644 index 00000000000..26611934cd9 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_file_format/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("error-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_file_format/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_file_format/scenario.yaml new file mode 100644 index 00000000000..1c25b60f0bf --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_file_format/scenario.yaml @@ -0,0 +1,13 @@ +description: > + An unsupported major `file_format` version fails configuration. +until: + - exited: {} +expect: + - output: + stream: stderr + pattern: Failed to auto initialize OpenTelemetry + - output: + stream: stderr + pattern: "Unsupported file_format '2.0'" + - spans: + count: 0 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_sampler/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_sampler/otel-config.yaml new file mode 100644 index 00000000000..ba02dc86692 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_sampler/otel-config.yaml @@ -0,0 +1,16 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-error-unsupported-sampler +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces + sampler: + jaeger_remote/development: + endpoint: http://localhost:14250 + initial_sampler: + always_on: diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_sampler/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_sampler/run.py new file mode 100644 index 00000000000..26611934cd9 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_sampler/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("error-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_sampler/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_sampler/scenario.yaml new file mode 100644 index 00000000000..4a68a8cf78d --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/error_unsupported_sampler/scenario.yaml @@ -0,0 +1,13 @@ +description: > + The `jaeger_remote/development` sampler is unsupported and fails configuration. +until: + - exited: {} +expect: + - output: + stream: stderr + pattern: Failed to auto initialize OpenTelemetry + - output: + stream: stderr + pattern: "Unsupported sampler type" + - spans: + count: 0 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_attribute_limits/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_attribute_limits/otel-config.yaml new file mode 100644 index 00000000000..995359c3457 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_attribute_limits/otel-config.yaml @@ -0,0 +1,20 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-general-attribute-limits +attribute_limits: + attribute_count_limit: 2 + attribute_value_length_limit: 4 +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces +logger_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/logs diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_attribute_limits/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_attribute_limits/run.py new file mode 100644 index 00000000000..3bdcd5e231e --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_attribute_limits/run.py @@ -0,0 +1,13 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace +from opentelemetry._logs import get_logger + +# Once the count limit is reached, the oldest attribute is dropped. +attributes = {"dropped": 0, "a": "abcdefgh", "b": 1} + +with trace.get_tracer(__name__).start_as_current_span("limited-span", attributes=attributes): + pass + +get_logger(__name__).emit(body="limited log", attributes=attributes) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_attribute_limits/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_attribute_limits/scenario.yaml new file mode 100644 index 00000000000..ea311115c39 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_attribute_limits/scenario.yaml @@ -0,0 +1,24 @@ +description: > + Global `attribute_limits` apply to span and log record attributes. +until: + - spans: + name: limited-span + - logs: + body_pattern: limited log +expect: + - spans: + name: limited-span + attributes: + values: + dropped: {exists: false} + a: abcd + b: 1 + count: 1 + - logs: + body: limited log + attributes: + values: + dropped: {exists: false} + a: abcd + b: 1 + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_disabled/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_disabled/otel-config.yaml new file mode 100644 index 00000000000..a60740ae65e --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_disabled/otel-config.yaml @@ -0,0 +1,12 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-general-disabled +disabled: true +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_disabled/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_disabled/run.py new file mode 100644 index 00000000000..a051696fe65 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_disabled/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("disabled-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_disabled/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_disabled/scenario.yaml new file mode 100644 index 00000000000..446480d62c6 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_disabled/scenario.yaml @@ -0,0 +1,8 @@ +description: > + `disabled: true` leaves the SDK unconfigured, so nothing is exported. +until: + - exited: {} +expect: + - spans: + name: disabled-span + count: 0 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_env_substitution/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_env_substitution/otel-config.yaml new file mode 100644 index 00000000000..d361c9ec4cc --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_env_substitution/otel-config.yaml @@ -0,0 +1,23 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: ${DECLCONFIG_SERVICE_NAME} + - name: env.standalone + value: ${DECLCONFIG_INT} + - name: env.quoted + value: "${DECLCONFIG_INT}" + - name: env.embedded + value: v-${DECLCONFIG_INT} + - name: env.default + value: ${DECLCONFIG_UNSET:-fallback} + - name: env.empty + value: "${DECLCONFIG_EMPTY:-fallback}" + - name: env.escaped + value: $${DECLCONFIG_INT} +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_env_substitution/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_env_substitution/run.py new file mode 100644 index 00000000000..e0b5a69c45b --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_env_substitution/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("env-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_env_substitution/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_env_substitution/scenario.yaml new file mode 100644 index 00000000000..5c7d58961f6 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_env_substitution/scenario.yaml @@ -0,0 +1,21 @@ +description: > + Environment variable substitution: plain, typed standalone, embedded, quoted, defaults, + set-but-empty and `$$` escapes. +env: + DECLCONFIG_SERVICE_NAME: declconfig-general-env-substitution + DECLCONFIG_INT: "42" + DECLCONFIG_EMPTY: "" +until: + - spans: + name: env-span +expect: + - resource: + attributes: + values: + service.name: declconfig-general-env-substitution + env.standalone: 42 + env.quoted: "42" + env.embedded: v-42 + env.default: fallback + env.empty: "" + env.escaped: ${DECLCONFIG_INT} diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_file_format_minor/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_file_format_minor/otel-config.yaml new file mode 100644 index 00000000000..2b751271320 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_file_format_minor/otel-config.yaml @@ -0,0 +1,11 @@ +file_format: "1.99" +resource: + attributes: + - name: service.name + value: declconfig-general-file-format-minor +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_file_format_minor/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_file_format_minor/run.py new file mode 100644 index 00000000000..78691b96471 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_file_format_minor/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("minor-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_file_format_minor/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_file_format_minor/scenario.yaml new file mode 100644 index 00000000000..0cf724c98d3 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_file_format_minor/scenario.yaml @@ -0,0 +1,12 @@ +description: > + A newer minor `file_format` version is accepted with a warning. +until: + - spans: + name: minor-span +expect: + - spans: + name: minor-span + count: 1 + - output: + stream: stderr + pattern: "1\\.99" diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_json_config/otel-config.json b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_json_config/otel-config.json new file mode 100644 index 00000000000..96c7f5cbb99 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_json_config/otel-config.json @@ -0,0 +1,11 @@ +{ + "file_format": "1.0", + "resource": { + "attributes": [{"name": "service.name", "value": "declconfig-general-json-config"}] + }, + "tracer_provider": { + "processors": [ + {"simple": {"exporter": {"otlp_http": {"endpoint": "http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces"}}}} + ] + } +} diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_json_config/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_json_config/run.py new file mode 100644 index 00000000000..b69342a6b11 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_json_config/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("json-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_json_config/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_json_config/scenario.yaml new file mode 100644 index 00000000000..88b1aac61c2 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/general_json_config/scenario.yaml @@ -0,0 +1,13 @@ +description: > + A JSON config file (`otel-config.json`) configures the SDK like a YAML one. +until: + - spans: + name: json-span +expect: + - resource: + attributes: + values: + service.name: declconfig-general-json-config + - spans: + name: json-span + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_batch_otlp_http/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_batch_otlp_http/otel-config.yaml new file mode 100644 index 00000000000..4ee996e9374 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_batch_otlp_http/otel-config.yaml @@ -0,0 +1,21 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-log-batch-otlp-http +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces +logger_provider: + processors: + - batch: + schedule_delay: 100 + export_timeout: 5000 + max_queue_size: 64 + max_export_batch_size: 8 + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/logs diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_batch_otlp_http/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_batch_otlp_http/run.py new file mode 100644 index 00000000000..c3023851768 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_batch_otlp_http/run.py @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace +from opentelemetry._logs import SeverityNumber, get_logger + +logger = get_logger(__name__) + +with trace.get_tracer(__name__).start_as_current_span("handler"): + logger.emit( + body="user logged in", + severity_number=SeverityNumber.WARN, + severity_text="WARN", + event_name="user.login", + attributes={"user.id": "u-1"}, + ) +logger.emit(body={"kind": "structured", "count": 2}, severity_number=SeverityNumber.INFO) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_batch_otlp_http/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_batch_otlp_http/scenario.yaml new file mode 100644 index 00000000000..9a14d32e676 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_batch_otlp_http/scenario.yaml @@ -0,0 +1,22 @@ +description: > + Exports log records over OTLP/HTTP through a batch processor with every batch setting, + correlated with the active span. +until: + - logs: + min_count: 2 +expect: + - logs: + body: user logged in + severity_number: WARN + severity_text: WARN + event_name: user.login + trace_context: true + span: {name: handler} + scope: {name: __main__} + attributes: {values: {user.id: u-1}, strict: true} + count: 1 + - logs: + body: {kvlist: {kind: structured, count: 2}} + severity_number: INFO + trace_context: false + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_console/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_console/otel-config.yaml new file mode 100644 index 00000000000..2fb8ab9d211 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_console/otel-config.yaml @@ -0,0 +1,10 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-log-console +logger_provider: + processors: + - simple: + exporter: + console: diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_console/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_console/run.py new file mode 100644 index 00000000000..a1528725936 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_console/run.py @@ -0,0 +1,6 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry._logs import get_logger + +get_logger(__name__).emit(body="console log") diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_console/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_console/scenario.yaml new file mode 100644 index 00000000000..c474c5e5221 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_console/scenario.yaml @@ -0,0 +1,8 @@ +description: > + The console log exporter writes log records to stdout. +until: + - exited: {} +expect: + - output: + stream: stdout + pattern: '"body": "console log"' diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_limits/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_limits/otel-config.yaml new file mode 100644 index 00000000000..2c34d48db5b --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_limits/otel-config.yaml @@ -0,0 +1,17 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-log-limits +attribute_limits: + attribute_count_limit: 10 + attribute_value_length_limit: 100 +logger_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/logs + limits: + attribute_count_limit: 2 + attribute_value_length_limit: 4 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_limits/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_limits/run.py new file mode 100644 index 00000000000..96995969528 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_limits/run.py @@ -0,0 +1,7 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry._logs import get_logger + +# Once the count limit is reached, the oldest attribute is dropped. +get_logger(__name__).emit(body="limited log", attributes={"dropped": 0, "a": "abcdefgh", "b": 1}) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_limits/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_limits/scenario.yaml new file mode 100644 index 00000000000..0396eb389da --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_limits/scenario.yaml @@ -0,0 +1,14 @@ +description: > + Log record `limits` cap attribute count and length, overriding the global `attribute_limits`. +until: + - logs: + body_pattern: limited log +expect: + - logs: + body: limited log + attributes: + values: + a: abcd + b: 1 + strict: true + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_logger_configurator/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_logger_configurator/otel-config.yaml new file mode 100644 index 00000000000..4b7f5e9fc6f --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_logger_configurator/otel-config.yaml @@ -0,0 +1,18 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-log-logger-configurator +logger_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/logs + logger_configurator/development: + default_config: + enabled: true + loggers: + - name: disabled.* + config: + enabled: false diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_logger_configurator/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_logger_configurator/run.py new file mode 100644 index 00000000000..26bcb5ae9d7 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_logger_configurator/run.py @@ -0,0 +1,7 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry._logs import get_logger + +get_logger("enabled.lib").emit(body="enabled log") +get_logger("disabled.lib").emit(body="disabled log") diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_logger_configurator/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_logger_configurator/scenario.yaml new file mode 100644 index 00000000000..82495a5c6bf --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_logger_configurator/scenario.yaml @@ -0,0 +1,12 @@ +description: > + `logger_configurator/development` disables loggers whose name matches a glob. +until: + - exited: {} +expect: + - logs: + body: enabled log + scope: {name: enabled.lib} + count: 1 + - logs: + body: disabled log + count: 0 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_simple_otlp_grpc/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_simple_otlp_grpc/otel-config.yaml new file mode 100644 index 00000000000..ad448353267 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_simple_otlp_grpc/otel-config.yaml @@ -0,0 +1,11 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-log-simple-otlp-grpc +logger_provider: + processors: + - simple: + exporter: + otlp_grpc: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4317 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_simple_otlp_grpc/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_simple_otlp_grpc/run.py new file mode 100644 index 00000000000..a44a52fdc5b --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_simple_otlp_grpc/run.py @@ -0,0 +1,6 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry._logs import get_logger + +get_logger(__name__).emit(body="grpc log") diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_simple_otlp_grpc/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_simple_otlp_grpc/scenario.yaml new file mode 100644 index 00000000000..0c2da18eb99 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/log_simple_otlp_grpc/scenario.yaml @@ -0,0 +1,9 @@ +description: > + Exports a log record over OTLP/gRPC through a simple processor. +until: + - logs: + body_pattern: grpc log +expect: + - logs: + body: grpc log + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_console/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_console/otel-config.yaml new file mode 100644 index 00000000000..80067428173 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_console/otel-config.yaml @@ -0,0 +1,11 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-metric-console +meter_provider: + readers: + - periodic: + interval: 500 + exporter: + console: diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_console/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_console/run.py new file mode 100644 index 00000000000..f70c72cba97 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_console/run.py @@ -0,0 +1,6 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import metrics + +metrics.get_meter(__name__).create_counter("console.requests").add(1) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_console/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_console/scenario.yaml new file mode 100644 index 00000000000..cfbc8c979db --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_console/scenario.yaml @@ -0,0 +1,8 @@ +description: > + The console metric exporter writes metrics to stdout. +until: + - exited: {} +expect: + - output: + stream: stdout + pattern: '"name": "console.requests"' diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_default_histogram_exponential/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_default_histogram_exponential/otel-config.yaml new file mode 100644 index 00000000000..0225f8e624b --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_default_histogram_exponential/otel-config.yaml @@ -0,0 +1,13 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-metric-default-histogram-exponential +meter_provider: + readers: + - periodic: + interval: 500 + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics + default_histogram_aggregation: base2_exponential_bucket_histogram diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_default_histogram_exponential/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_default_histogram_exponential/run.py new file mode 100644 index 00000000000..129d83570f7 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_default_histogram_exponential/run.py @@ -0,0 +1,8 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import metrics + +latency = metrics.get_meter(__name__).create_histogram("latency", unit="ms") +latency.record(10) +latency.record(20) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_default_histogram_exponential/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_default_histogram_exponential/scenario.yaml new file mode 100644 index 00000000000..db16a9f8fa3 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_default_histogram_exponential/scenario.yaml @@ -0,0 +1,13 @@ +description: > + `default_histogram_aggregation: base2_exponential_bucket_histogram` exports histograms as + exponential histograms. +until: + - metric: + name: latency + field: count + min: 2 +expect: + - metric: + name: latency + type: exponential_histogram + data_point: {count: 2, sum: 30, min: 10, max: 20} diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_off/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_off/otel-config.yaml new file mode 100644 index 00000000000..e884dcc591d --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_off/otel-config.yaml @@ -0,0 +1,19 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-metric-exemplar-filter-always-off +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces +meter_provider: + readers: + - periodic: + interval: 500 + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics + exemplar_filter: always_off diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_off/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_off/run.py new file mode 100644 index 00000000000..4b35cc23461 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_off/run.py @@ -0,0 +1,12 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import metrics, trace + +meter = metrics.get_meter(__name__) +inside = meter.create_counter("inside.requests") +outside = meter.create_counter("outside.requests") + +with trace.get_tracer(__name__).start_as_current_span("exemplar-span"): + inside.add(1) +outside.add(1) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_off/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_off/scenario.yaml new file mode 100644 index 00000000000..290e933db88 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_off/scenario.yaml @@ -0,0 +1,14 @@ +description: > + `exemplar_filter: always_off` records no exemplars, even in a sampled span. +until: + - metric: + name: inside.requests + min: 1 + - metric: + name: outside.requests + min: 1 +expect: + - metric: + name: inside.requests + data_point: {exemplar_count: {gte: 1}} + count: 0 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_on/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_on/otel-config.yaml new file mode 100644 index 00000000000..c88b1e29b4f --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_on/otel-config.yaml @@ -0,0 +1,19 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-metric-exemplar-filter-always-on +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces +meter_provider: + readers: + - periodic: + interval: 500 + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics + exemplar_filter: always_on diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_on/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_on/run.py new file mode 100644 index 00000000000..4b35cc23461 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_on/run.py @@ -0,0 +1,12 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import metrics, trace + +meter = metrics.get_meter(__name__) +inside = meter.create_counter("inside.requests") +outside = meter.create_counter("outside.requests") + +with trace.get_tracer(__name__).start_as_current_span("exemplar-span"): + inside.add(1) +outside.add(1) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_on/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_on/scenario.yaml new file mode 100644 index 00000000000..8ca63a11177 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_always_on/scenario.yaml @@ -0,0 +1,13 @@ +description: > + `exemplar_filter: always_on` records exemplars for every measurement. +until: + - metric: + name: inside.requests + min: 1 + - metric: + name: outside.requests + min: 1 +expect: + - metric: + name: outside.requests + data_point: {exemplar_count: {gte: 1}} diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_trace_based/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_trace_based/otel-config.yaml new file mode 100644 index 00000000000..427b0a63ffc --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_trace_based/otel-config.yaml @@ -0,0 +1,19 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-metric-exemplar-filter-trace-based +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces +meter_provider: + readers: + - periodic: + interval: 500 + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics + exemplar_filter: trace_based diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_trace_based/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_trace_based/run.py new file mode 100644 index 00000000000..4b35cc23461 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_trace_based/run.py @@ -0,0 +1,12 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import metrics, trace + +meter = metrics.get_meter(__name__) +inside = meter.create_counter("inside.requests") +outside = meter.create_counter("outside.requests") + +with trace.get_tracer(__name__).start_as_current_span("exemplar-span"): + inside.add(1) +outside.add(1) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_trace_based/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_trace_based/scenario.yaml new file mode 100644 index 00000000000..ce1705291c5 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_exemplar_filter_trace_based/scenario.yaml @@ -0,0 +1,17 @@ +description: > + `exemplar_filter: trace_based` records exemplars only for measurements in a sampled span. +until: + - metric: + name: inside.requests + min: 1 + - metric: + name: outside.requests + min: 1 +expect: + - metric: + name: inside.requests + data_point: {exemplar_count: {gte: 1}} + - metric: + name: outside.requests + data_point: {exemplar_count: {gte: 1}} + count: 0 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_instruments_cumulative/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_instruments_cumulative/otel-config.yaml new file mode 100644 index 00000000000..b50efd53f3b --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_instruments_cumulative/otel-config.yaml @@ -0,0 +1,12 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-metric-instruments-cumulative +meter_provider: + readers: + - periodic: + interval: 500 + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_instruments_cumulative/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_instruments_cumulative/run.py new file mode 100644 index 00000000000..242260474df --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_instruments_cumulative/run.py @@ -0,0 +1,19 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import metrics +from opentelemetry.metrics import Observation + +meter = metrics.get_meter("declconfig.metrics") + +meter.create_counter("requests").add(3, {"route": "/a"}) +queue_size = meter.create_up_down_counter("queue.size") +queue_size.add(5) +queue_size.add(-2) +latency = meter.create_histogram("latency", unit="ms") +latency.record(10) +latency.record(20) +meter.create_gauge("temperature").set(21.5) +meter.create_observable_counter("cpu.time", [lambda options: [Observation(7)]]) +meter.create_observable_gauge("memory.usage", [lambda options: [Observation(1024)]]) +meter.create_observable_up_down_counter("connections", [lambda options: [Observation(4)]]) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_instruments_cumulative/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_instruments_cumulative/scenario.yaml new file mode 100644 index 00000000000..b66fffda60b --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_instruments_cumulative/scenario.yaml @@ -0,0 +1,48 @@ +description: > + Every instrument kind exported with the default cumulative temporality. +until: + - metric: + name: requests + min: 3 +expect: + - metric: + name: requests + type: sum + monotonic: true + temporality: CUMULATIVE + scope: {name: declconfig.metrics} + data_point: + value: 3 + attributes: {values: {route: /a}, strict: true} + - metric: + name: queue.size + type: sum + monotonic: false + temporality: CUMULATIVE + data_point: {value: 3} + - metric: + name: latency + type: histogram + unit: ms + temporality: CUMULATIVE + data_point: {count: 2, sum: 30, min: 10, max: 20} + - metric: + name: temperature + type: gauge + data_point: {value: 21.5} + - metric: + name: cpu.time + type: sum + monotonic: true + temporality: CUMULATIVE + data_point: {value: 7} + - metric: + name: memory.usage + type: gauge + data_point: {value: 1024} + - metric: + name: connections + type: sum + monotonic: false + temporality: CUMULATIVE + data_point: {value: 4} diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_meter_configurator/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_meter_configurator/otel-config.yaml new file mode 100644 index 00000000000..3fa0746d388 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_meter_configurator/otel-config.yaml @@ -0,0 +1,19 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-metric-meter-configurator +meter_provider: + readers: + - periodic: + interval: 500 + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics + meter_configurator/development: + default_config: + enabled: true + meters: + - name: disabled.* + config: + enabled: false diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_meter_configurator/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_meter_configurator/run.py new file mode 100644 index 00000000000..755ac3b2820 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_meter_configurator/run.py @@ -0,0 +1,7 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import metrics + +metrics.get_meter("enabled.meter").create_counter("enabled.requests").add(1) +metrics.get_meter("disabled.meter").create_counter("disabled.requests").add(1) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_meter_configurator/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_meter_configurator/scenario.yaml new file mode 100644 index 00000000000..e0aaa1b4d7b --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_meter_configurator/scenario.yaml @@ -0,0 +1,13 @@ +description: > + `meter_configurator/development` disables meters whose name matches a glob. +until: + - metric: + name: enabled.requests + min: 1 +expect: + - metric: + name: enabled.requests + scope: {name: enabled.meter} + - metric: + name: disabled.requests + count: 0 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_otlp_grpc/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_otlp_grpc/otel-config.yaml new file mode 100644 index 00000000000..4a3dd40e7ef --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_otlp_grpc/otel-config.yaml @@ -0,0 +1,13 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-metric-otlp-grpc +meter_provider: + readers: + - periodic: + interval: 500 + exporter: + otlp_grpc: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4317 + compression: gzip diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_otlp_grpc/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_otlp_grpc/run.py new file mode 100644 index 00000000000..417d6fa1b32 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_otlp_grpc/run.py @@ -0,0 +1,6 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import metrics + +metrics.get_meter(__name__).create_counter("grpc.requests").add(1) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_otlp_grpc/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_otlp_grpc/scenario.yaml new file mode 100644 index 00000000000..84abba26d51 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_otlp_grpc/scenario.yaml @@ -0,0 +1,11 @@ +description: > + Exports metrics over OTLP/gRPC with gzip compression. +until: + - metric: + name: grpc.requests + min: 1 +expect: + - metric: + name: grpc.requests + type: sum + data_point: {value: 1} diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_prometheus_pull/collector-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_prometheus_pull/collector-config.yaml new file mode 100644 index 00000000000..fe4b3e392d5 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_prometheus_pull/collector-config.yaml @@ -0,0 +1,32 @@ +extensions: + health_check: + endpoint: 0.0.0.0:13133 + +receivers: + prometheus: + config: + scrape_configs: + - job_name: declconfig + scrape_interval: 1s + static_configs: + - targets: [host.docker.internal:9464] + +processors: + # The receiver reports these on every scrape, even once the app has stopped. + filter/scrape: + error_mode: ignore + metrics: + metric: + - 'name == "up" or IsMatch(name, "^scrape_")' + +exporters: + otlphttp: + endpoint: ${env:OTLP_SINK_ENDPOINT} + +service: + extensions: [health_check] + pipelines: + metrics: + receivers: [prometheus] + processors: [filter/scrape] + exporters: [otlphttp] diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_prometheus_pull/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_prometheus_pull/otel-config.yaml new file mode 100644 index 00000000000..10712b71530 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_prometheus_pull/otel-config.yaml @@ -0,0 +1,13 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-metric-prometheus-pull +meter_provider: + readers: + - pull: + exporter: + prometheus/development: + host: 0.0.0.0 + port: 9464 + target_info_enabled/development: false diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_prometheus_pull/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_prometheus_pull/run.py new file mode 100644 index 00000000000..0f44289f7ff --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_prometheus_pull/run.py @@ -0,0 +1,12 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +import time + +from opentelemetry import metrics + +requests = metrics.get_meter(__name__).create_counter("prom.requests") +# Keep serving until the harness stops the app. +while True: + requests.add(1) + time.sleep(0.5) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_prometheus_pull/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_prometheus_pull/scenario.yaml new file mode 100644 index 00000000000..d1dc47553c1 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_prometheus_pull/scenario.yaml @@ -0,0 +1,12 @@ +description: > + A `pull` reader with the `prometheus/development` exporter serves metrics for the collector's + prometheus receiver to scrape. +until: + - metric: + name: prom_requests_total + min: 1 +expect: + - metric: + name: prom_requests_total + type: sum + monotonic: true diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_delta/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_delta/otel-config.yaml new file mode 100644 index 00000000000..67f585b7f8f --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_delta/otel-config.yaml @@ -0,0 +1,13 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-metric-temporality-delta +meter_provider: + readers: + - periodic: + interval: 500 + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics + temporality_preference: delta diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_delta/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_delta/run.py new file mode 100644 index 00000000000..242260474df --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_delta/run.py @@ -0,0 +1,19 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import metrics +from opentelemetry.metrics import Observation + +meter = metrics.get_meter("declconfig.metrics") + +meter.create_counter("requests").add(3, {"route": "/a"}) +queue_size = meter.create_up_down_counter("queue.size") +queue_size.add(5) +queue_size.add(-2) +latency = meter.create_histogram("latency", unit="ms") +latency.record(10) +latency.record(20) +meter.create_gauge("temperature").set(21.5) +meter.create_observable_counter("cpu.time", [lambda options: [Observation(7)]]) +meter.create_observable_gauge("memory.usage", [lambda options: [Observation(1024)]]) +meter.create_observable_up_down_counter("connections", [lambda options: [Observation(4)]]) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_delta/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_delta/scenario.yaml new file mode 100644 index 00000000000..b0923a1c0e7 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_delta/scenario.yaml @@ -0,0 +1,13 @@ +description: > + `temporality_preference: delta` exports counters, histograms and observable counters as delta, + and up-down counters as cumulative. +until: + - metric: + name: requests + min: 3 +expect: + - metric: {name: requests, temporality: DELTA} + - metric: {name: latency, temporality: DELTA} + - metric: {name: cpu.time, temporality: DELTA} + - metric: {name: queue.size, temporality: CUMULATIVE} + - metric: {name: connections, temporality: CUMULATIVE} diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_low_memory/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_low_memory/otel-config.yaml new file mode 100644 index 00000000000..13e6bf16913 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_low_memory/otel-config.yaml @@ -0,0 +1,13 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-metric-temporality-low-memory +meter_provider: + readers: + - periodic: + interval: 500 + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics + temporality_preference: low_memory diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_low_memory/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_low_memory/run.py new file mode 100644 index 00000000000..242260474df --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_low_memory/run.py @@ -0,0 +1,19 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import metrics +from opentelemetry.metrics import Observation + +meter = metrics.get_meter("declconfig.metrics") + +meter.create_counter("requests").add(3, {"route": "/a"}) +queue_size = meter.create_up_down_counter("queue.size") +queue_size.add(5) +queue_size.add(-2) +latency = meter.create_histogram("latency", unit="ms") +latency.record(10) +latency.record(20) +meter.create_gauge("temperature").set(21.5) +meter.create_observable_counter("cpu.time", [lambda options: [Observation(7)]]) +meter.create_observable_gauge("memory.usage", [lambda options: [Observation(1024)]]) +meter.create_observable_up_down_counter("connections", [lambda options: [Observation(4)]]) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_low_memory/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_low_memory/scenario.yaml new file mode 100644 index 00000000000..865155ee3e5 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_temporality_low_memory/scenario.yaml @@ -0,0 +1,13 @@ +description: > + `temporality_preference: low_memory` exports synchronous counters and histograms as delta, + and everything else as cumulative. +until: + - metric: + name: requests + min: 3 +expect: + - metric: {name: requests, temporality: DELTA} + - metric: {name: latency, temporality: DELTA} + - metric: {name: cpu.time, temporality: CUMULATIVE} + - metric: {name: queue.size, temporality: CUMULATIVE} + - metric: {name: connections, temporality: CUMULATIVE} diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_aggregations/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_aggregations/otel-config.yaml new file mode 100644 index 00000000000..8941dc2cf39 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_aggregations/otel-config.yaml @@ -0,0 +1,38 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-metric-views-aggregations +meter_provider: + readers: + - periodic: + interval: 500 + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics + views: + - selector: {instrument_name: dropped} + stream: + aggregation: + drop: + - selector: {instrument_name: summed} + stream: + aggregation: + sum: + - selector: {instrument_name: last} + stream: + aggregation: + last_value: + - selector: {instrument_name: bucketed} + stream: + aggregation: + explicit_bucket_histogram: + boundaries: [10, 100] + record_min_max: false + - selector: {instrument_name: exponential} + stream: + aggregation: + base2_exponential_bucket_histogram: + max_size: 4 + max_scale: 2 + record_min_max: true diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_aggregations/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_aggregations/run.py new file mode 100644 index 00000000000..9d0db17c454 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_aggregations/run.py @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import metrics + +meter = metrics.get_meter(__name__) +meter.create_counter("dropped").add(1) +summed = meter.create_histogram("summed") +summed.record(10) +summed.record(20) +last = meter.create_counter("last") +last.add(1) +last.add(2) +for name in ("bucketed", "exponential"): + histogram = meter.create_histogram(name) + histogram.record(5) + histogram.record(50) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_aggregations/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_aggregations/scenario.yaml new file mode 100644 index 00000000000..084371d5407 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_aggregations/scenario.yaml @@ -0,0 +1,35 @@ +description: > + Views apply drop, sum, last_value, explicit bucket histogram and base2 exponential bucket + histogram aggregations. +until: + - metric: + name: summed + min: 30 + - metric: + name: last + min: 2 +expect: + - metric: + name: dropped + count: 0 + - metric: + name: summed + type: sum + data_point: {value: 30} + - metric: + name: last + type: gauge + data_point: {value: 2} + - metric: + name: bucketed + type: histogram + data_point: + count: 2 + explicit_bounds: [10, 100] + bucket_counts: [1, 1, 0] + min: {exists: false} + max: {exists: false} + - metric: + name: exponential + type: exponential_histogram + data_point: {count: 2} diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_stream/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_stream/otel-config.yaml new file mode 100644 index 00000000000..a1db7638f60 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_stream/otel-config.yaml @@ -0,0 +1,29 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-metric-views-stream +meter_provider: + readers: + - periodic: + interval: 500 + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/metrics + views: + - selector: + instrument_name: requests + instrument_type: counter + unit: "1" + meter_name: declconfig.views + meter_version: "1.0" + stream: + name: renamed.requests + description: Renamed by a view + attribute_keys: + included: [route] + - selector: + instrument_name: unmatched + meter_name: other.meter + stream: + name: should.not.apply diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_stream/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_stream/run.py new file mode 100644 index 00000000000..650af70fe8b --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_stream/run.py @@ -0,0 +1,8 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import metrics + +meter = metrics.get_meter("declconfig.views", version="1.0") +meter.create_counter("requests", unit="1").add(1, {"route": "/a", "method": "GET"}) +meter.create_counter("unmatched").add(1) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_stream/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_stream/scenario.yaml new file mode 100644 index 00000000000..52e924b5746 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/metric_views_stream/scenario.yaml @@ -0,0 +1,24 @@ +description: > + A view selects an instrument by name, type, unit and meter name/version, and renames it, + sets its description and keeps only the included attribute keys. A view whose selector + doesn't match has no effect. +until: + - metric: + name: renamed.requests + min: 1 + - metric: + name: unmatched + min: 1 +expect: + - metric: + name: renamed.requests + type: sum + description: Renamed by a view + data_point: + attributes: {values: {route: /a}, strict: true} + - metric: + name: requests + count: 0 + - metric: + name: should.not.apply + count: 0 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_absent/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_absent/otel-config.yaml new file mode 100644 index 00000000000..6e3f95ec242 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_absent/otel-config.yaml @@ -0,0 +1,11 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-propagator-absent +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_absent/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_absent/run.py new file mode 100644 index 00000000000..303f1fa59bb --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_absent/run.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import baggage, propagate, trace + +tracer = trace.get_tracer(__name__) + +carrier: dict[str, str] = {} +with tracer.start_as_current_span("outgoing"): + propagate.inject(carrier, context=baggage.set_baggage("user.id", "u-123")) + +extracted = propagate.extract(carrier) +attributes = { + "carrier.keys": ",".join(sorted(carrier)) or "", + "baggage.user.id": str(baggage.get_baggage("user.id", extracted) or ""), +} +with tracer.start_as_current_span("incoming", context=extracted, attributes=attributes): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_absent/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_absent/scenario.yaml new file mode 100644 index 00000000000..14d3155239c --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_absent/scenario.yaml @@ -0,0 +1,14 @@ +description: > + Without a `propagator` section nothing is propagated. +until: + - spans: + name: incoming +expect: + - spans: + name: incoming + root: true + attributes: + values: + carrier.keys: + baggage.user.id: + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_b3_single/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_b3_single/otel-config.yaml new file mode 100644 index 00000000000..604e664cf2f --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_b3_single/otel-config.yaml @@ -0,0 +1,14 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-propagator-b3-single +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces +propagator: + composite: + - b3: diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_b3_single/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_b3_single/run.py new file mode 100644 index 00000000000..303f1fa59bb --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_b3_single/run.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import baggage, propagate, trace + +tracer = trace.get_tracer(__name__) + +carrier: dict[str, str] = {} +with tracer.start_as_current_span("outgoing"): + propagate.inject(carrier, context=baggage.set_baggage("user.id", "u-123")) + +extracted = propagate.extract(carrier) +attributes = { + "carrier.keys": ",".join(sorted(carrier)) or "", + "baggage.user.id": str(baggage.get_baggage("user.id", extracted) or ""), +} +with tracer.start_as_current_span("incoming", context=extracted, attributes=attributes): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_b3_single/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_b3_single/scenario.yaml new file mode 100644 index 00000000000..dcc1883acb8 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_b3_single/scenario.yaml @@ -0,0 +1,14 @@ +description: > + `composite: [b3]` propagates the span context in the single `b3` header, without baggage. +until: + - spans: + name: incoming +expect: + - spans: + name: incoming + parent: {name: outgoing} + attributes: + values: + carrier.keys: b3 + baggage.user.id: + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_composite_list/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_composite_list/otel-config.yaml new file mode 100644 index 00000000000..71500618e5d --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_composite_list/otel-config.yaml @@ -0,0 +1,13 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-propagator-composite-list +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces +propagator: + composite_list: b3multi,jaeger diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_composite_list/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_composite_list/run.py new file mode 100644 index 00000000000..303f1fa59bb --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_composite_list/run.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import baggage, propagate, trace + +tracer = trace.get_tracer(__name__) + +carrier: dict[str, str] = {} +with tracer.start_as_current_span("outgoing"): + propagate.inject(carrier, context=baggage.set_baggage("user.id", "u-123")) + +extracted = propagate.extract(carrier) +attributes = { + "carrier.keys": ",".join(sorted(carrier)) or "", + "baggage.user.id": str(baggage.get_baggage("user.id", extracted) or ""), +} +with tracer.start_as_current_span("incoming", context=extracted, attributes=attributes): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_composite_list/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_composite_list/scenario.yaml new file mode 100644 index 00000000000..fe3312bdd39 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_composite_list/scenario.yaml @@ -0,0 +1,14 @@ +description: > + `composite_list` loads b3multi and jaeger propagators through entry points. +until: + - spans: + name: incoming +expect: + - spans: + name: incoming + parent: {name: outgoing} + attributes: + values: + carrier.keys: uber-trace-id,uberctx-user.id,x-b3-sampled,x-b3-spanid,x-b3-traceid + baggage.user.id: u-123 + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_tracecontext_baggage/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_tracecontext_baggage/otel-config.yaml new file mode 100644 index 00000000000..0cf8d368ca6 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_tracecontext_baggage/otel-config.yaml @@ -0,0 +1,15 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-propagator-tracecontext-baggage +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces +propagator: + composite: + - tracecontext: + - baggage: diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_tracecontext_baggage/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_tracecontext_baggage/run.py new file mode 100644 index 00000000000..303f1fa59bb --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_tracecontext_baggage/run.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import baggage, propagate, trace + +tracer = trace.get_tracer(__name__) + +carrier: dict[str, str] = {} +with tracer.start_as_current_span("outgoing"): + propagate.inject(carrier, context=baggage.set_baggage("user.id", "u-123")) + +extracted = propagate.extract(carrier) +attributes = { + "carrier.keys": ",".join(sorted(carrier)) or "", + "baggage.user.id": str(baggage.get_baggage("user.id", extracted) or ""), +} +with tracer.start_as_current_span("incoming", context=extracted, attributes=attributes): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_tracecontext_baggage/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_tracecontext_baggage/scenario.yaml new file mode 100644 index 00000000000..ca89c0faafa --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/propagator_tracecontext_baggage/scenario.yaml @@ -0,0 +1,14 @@ +description: > + `composite: [tracecontext, baggage]` propagates the span context and baggage. +until: + - spans: + name: incoming +expect: + - spans: + name: incoming + parent: {name: outgoing} + attributes: + values: + carrier.keys: baggage,traceparent + baggage.user.id: u-123 + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_list/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_list/otel-config.yaml new file mode 100644 index 00000000000..343ca79c8d9 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_list/otel-config.yaml @@ -0,0 +1,12 @@ +file_format: "1.0" +resource: + attributes: + - name: list.conflict + value: from-attributes + attributes_list: service.name=declconfig-resource-attributes-list,list.encoded=hello%20world,list.number=5,list.conflict=from-list +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_list/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_list/run.py new file mode 100644 index 00000000000..3d3644d0f63 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_list/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("list-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_list/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_list/scenario.yaml new file mode 100644 index 00000000000..87a2c1ba054 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_list/scenario.yaml @@ -0,0 +1,13 @@ +description: > + `attributes_list` adds URL-decoded string attributes; `attributes` wins on conflicts. +until: + - spans: + name: list-span +expect: + - resource: + attributes: + values: + service.name: declconfig-resource-attributes-list + list.encoded: hello world + list.number: "5" + list.conflict: from-attributes diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_typed/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_typed/otel-config.yaml new file mode 100644 index 00000000000..adf1be40344 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_typed/otel-config.yaml @@ -0,0 +1,43 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-resource-attributes-typed + - name: typed.string + value: 12 + type: string + - name: typed.bool + value: "false" + type: bool + - name: typed.int + value: "12" + type: int + - name: typed.double + value: 12 + type: double + - name: typed.string_array + value: [1, 2] + type: string_array + - name: typed.bool_array + value: [true, false] + type: bool_array + - name: typed.int_array + value: [1, 2] + type: int_array + - name: typed.double_array + value: [1, 2.5] + type: double_array + - name: untyped.string + value: abc + - name: untyped.int + value: 3 + - name: untyped.double + value: 2.5 + - name: untyped.bool + value: true +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_typed/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_typed/run.py new file mode 100644 index 00000000000..33ab4ff9f30 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_typed/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("typed-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_typed/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_typed/scenario.yaml new file mode 100644 index 00000000000..03f7b43a3d7 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_attributes_typed/scenario.yaml @@ -0,0 +1,22 @@ +description: > + Resource attributes are coerced to their declared `type`; untyped values keep their YAML type. +until: + - spans: + name: typed-span +expect: + - resource: + attributes: + values: + service.name: declconfig-resource-attributes-typed + typed.string: "12" + typed.bool: false + typed.int: 12 + typed.double: 12.0 + typed.string_array: ["1", "2"] + typed.bool_array: [true, false] + typed.int_array: [1, 2] + typed.double_array: [1.0, 2.5] + untyped.string: abc + untyped.int: 3 + untyped.double: 2.5 + untyped.bool: true diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_default/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_default/otel-config.yaml new file mode 100644 index 00000000000..4bfbf278e29 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_default/otel-config.yaml @@ -0,0 +1,7 @@ +file_format: "1.0" +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_default/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_default/run.py new file mode 100644 index 00000000000..2a46c7f4067 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_default/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("default-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_default/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_default/scenario.yaml new file mode 100644 index 00000000000..fb40edda6c3 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_default/scenario.yaml @@ -0,0 +1,13 @@ +description: > + Without a `resource` section, the resource has the SDK attributes and a default service name. +until: + - spans: + name: default-span +expect: + - resource: + attributes: + values: + service.name: {pattern: "^unknown_service"} + telemetry.sdk.language: python + telemetry.sdk.name: opentelemetry + telemetry.sdk.version: {exists: true} diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_detectors/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_detectors/otel-config.yaml new file mode 100644 index 00000000000..d23695d714f --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_detectors/otel-config.yaml @@ -0,0 +1,22 @@ +file_format: "1.0" +resource: + detection/development: + detectors: + - service: + - host: + - process: + - os: + attributes: + included: + - service.* + - host.* + - process.* + - os.* + excluded: + - process.command* +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_detectors/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_detectors/run.py new file mode 100644 index 00000000000..c5a78623f06 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_detectors/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("detected-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_detectors/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_detectors/scenario.yaml new file mode 100644 index 00000000000..2c3b5aac4e0 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_detectors/scenario.yaml @@ -0,0 +1,21 @@ +description: > + `detection/development` runs the service, host, process and os detectors, filtered by + `included`/`excluded` globs. +env: + OTEL_SERVICE_NAME: declconfig-resource-detectors +until: + - spans: + name: detected-span +expect: + - resource: + attributes: + values: + service.name: declconfig-resource-detectors + service.instance.id: {exists: true} + host.name: {exists: true} + process.pid: {exists: true} + os.type: {exists: true} + process.command: {exists: false} + process.command_args: {exists: false} + process.command_line: {exists: false} + telemetry.sdk.language: python diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_schema_url/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_schema_url/otel-config.yaml new file mode 100644 index 00000000000..316c0f75955 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_schema_url/otel-config.yaml @@ -0,0 +1,12 @@ +file_format: "1.0" +resource: + schema_url: https://opentelemetry.io/schemas/1.26.0 + attributes: + - name: service.name + value: declconfig-resource-schema-url +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_schema_url/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_schema_url/run.py new file mode 100644 index 00000000000..4a85347d619 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_schema_url/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("schema-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_schema_url/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_schema_url/scenario.yaml new file mode 100644 index 00000000000..38830c8fe9a --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/resource_schema_url/scenario.yaml @@ -0,0 +1,11 @@ +description: > + `resource.schema_url` is exported with the resource. +until: + - spans: + name: schema-span +expect: + - resource: + schema_url: https://opentelemetry.io/schemas/1.26.0 + attributes: + values: + service.name: declconfig-resource-schema-url diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_batch_otlp_http/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_batch_otlp_http/otel-config.yaml new file mode 100644 index 00000000000..be9c73f3644 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_batch_otlp_http/otel-config.yaml @@ -0,0 +1,17 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-batch-otlp-http +tracer_provider: + processors: + - batch: + schedule_delay: 100 + export_timeout: 5000 + max_queue_size: 64 + max_export_batch_size: 8 + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces + id_generator: + random: diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_batch_otlp_http/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_batch_otlp_http/run.py new file mode 100644 index 00000000000..4cc12539aa2 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_batch_otlp_http/run.py @@ -0,0 +1,10 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +for _ in range(20): + with tracer.start_as_current_span("batch-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_batch_otlp_http/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_batch_otlp_http/scenario.yaml new file mode 100644 index 00000000000..b877dd7cc2e --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_batch_otlp_http/scenario.yaml @@ -0,0 +1,19 @@ +description: > + Exports spans over OTLP/HTTP through a batch span processor with every batch setting. +until: + - spans: + min_count: 20 + name: batch-span +expect: + - resource: + attributes: + values: + service.name: declconfig-trace-batch-otlp-http + telemetry.sdk.language: python + - spans: + name: batch-span + kind: INTERNAL + root: true + scope: + name: __main__ + count: 20 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_console/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_console/otel-config.yaml new file mode 100644 index 00000000000..a6ff5244541 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_console/otel-config.yaml @@ -0,0 +1,10 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-console +tracer_provider: + processors: + - simple: + exporter: + console: diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_console/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_console/run.py new file mode 100644 index 00000000000..9ac3249010e --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_console/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("console-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_console/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_console/scenario.yaml new file mode 100644 index 00000000000..ad5d571dcf3 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_console/scenario.yaml @@ -0,0 +1,8 @@ +description: > + The console span exporter writes spans to stdout. +until: + - exited: {} +expect: + - output: + stream: stdout + pattern: '"name": "console-span"' diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_multiple_processors/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_multiple_processors/otel-config.yaml new file mode 100644 index 00000000000..b96c6f93bea --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_multiple_processors/otel-config.yaml @@ -0,0 +1,15 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-multiple-processors +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces + - batch: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_multiple_processors/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_multiple_processors/run.py new file mode 100644 index 00000000000..8ee336a5ad9 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_multiple_processors/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("multi-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_multiple_processors/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_multiple_processors/scenario.yaml new file mode 100644 index 00000000000..e54f3db1c30 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_multiple_processors/scenario.yaml @@ -0,0 +1,10 @@ +description: > + Every configured span processor receives each span. +until: + - spans: + min_count: 2 + name: multi-span +expect: + - spans: + name: multi-span + count: 2 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_file_stdout/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_file_stdout/otel-config.yaml new file mode 100644 index 00000000000..b15a4259cb9 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_file_stdout/otel-config.yaml @@ -0,0 +1,11 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-otlp-file-stdout +tracer_provider: + processors: + - simple: + exporter: + otlp_file/development: + output_stream: stdout diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_file_stdout/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_file_stdout/run.py new file mode 100644 index 00000000000..81b24bb8c90 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_file_stdout/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("file-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_file_stdout/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_file_stdout/scenario.yaml new file mode 100644 index 00000000000..9f50ec1fdc9 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_file_stdout/scenario.yaml @@ -0,0 +1,8 @@ +description: > + The `otlp_file/development` span exporter writes OTLP JSON to stdout. +until: + - exited: {} +expect: + - output: + stream: stdout + pattern: '"name": ?"file-span"' diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_grpc/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_grpc/otel-config.yaml new file mode 100644 index 00000000000..68f490b3d6c --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_grpc/otel-config.yaml @@ -0,0 +1,16 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-otlp-grpc +tracer_provider: + processors: + - batch: + exporter: + otlp_grpc: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4317 + compression: gzip + timeout: 5000 + headers: + - name: x-declconfig + value: header diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_grpc/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_grpc/run.py new file mode 100644 index 00000000000..c7b03e9466a --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_grpc/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("grpc-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_grpc/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_grpc/scenario.yaml new file mode 100644 index 00000000000..cb5d6e584a4 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_grpc/scenario.yaml @@ -0,0 +1,9 @@ +description: > + Exports a span over OTLP/gRPC with gzip compression, headers and a timeout. +until: + - spans: + name: grpc-span +expect: + - spans: + name: grpc-span + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_http_options/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_http_options/otel-config.yaml new file mode 100644 index 00000000000..f9b06844719 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_http_options/otel-config.yaml @@ -0,0 +1,17 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-otlp-http-options +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces + compression: gzip + timeout: 5000 + headers: + - name: x-declconfig + value: header + headers_list: x-declconfig-list=list diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_http_options/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_http_options/run.py new file mode 100644 index 00000000000..58a303a3417 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_http_options/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("options-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_http_options/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_http_options/scenario.yaml new file mode 100644 index 00000000000..714b7348e3d --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_otlp_http_options/scenario.yaml @@ -0,0 +1,9 @@ +description: > + The OTLP/HTTP exporter accepts gzip compression, headers, headers_list and a timeout. +until: + - spans: + name: options-span +expect: + - spans: + name: options-span + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_always_off/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_always_off/otel-config.yaml new file mode 100644 index 00000000000..4ed89cd5d9b --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_always_off/otel-config.yaml @@ -0,0 +1,13 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-sampler-always-off +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces + sampler: + always_off: diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_always_off/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_always_off/run.py new file mode 100644 index 00000000000..7b0c19ded8b --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_always_off/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("dropped-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_always_off/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_always_off/scenario.yaml new file mode 100644 index 00000000000..7fb72a169d0 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_always_off/scenario.yaml @@ -0,0 +1,7 @@ +description: > + The `always_off` sampler drops every span. +until: + - exited: {} +expect: + - spans: + count: 0 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_probability/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_probability/otel-config.yaml new file mode 100644 index 00000000000..d4979262eca --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_probability/otel-config.yaml @@ -0,0 +1,31 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-sampler-composite-probability +tracer_provider: + processors: + - batch: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces + sampler: + composite/development: + rule_based: + rules: + - attribute_values: + key: sample + values: [never] + sampler: + probability: + ratio: 0 + - attribute_values: + key: sample + values: [always] + sampler: + probability: + ratio: 1 + - sampler: + parent_threshold: + root: + always_off: diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_probability/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_probability/run.py new file mode 100644 index 00000000000..c057528bc45 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_probability/run.py @@ -0,0 +1,17 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +for _ in range(10): + with tracer.start_as_current_span("never", attributes={"sample": "never"}): + pass +for index in range(10): + with tracer.start_as_current_span("always", attributes={"sample": "always"}): + if index == 0: + with tracer.start_as_current_span("threshold-child"): + pass +with tracer.start_as_current_span("threshold-root"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_probability/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_probability/scenario.yaml new file mode 100644 index 00000000000..e9a398ba6a9 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_probability/scenario.yaml @@ -0,0 +1,19 @@ +description: > + `composite/development` probability samplers with ratios 0 and 1, and `parent_threshold` + following the parent's decision and using its `root` sampler for root spans. +until: + - exited: {} +expect: + - spans: + name: never + count: 0 + - spans: + name: always + count: 10 + - spans: + name: threshold-root + count: 0 + - spans: + name: threshold-child + parent: {name: always} + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_rule_based/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_rule_based/otel-config.yaml new file mode 100644 index 00000000000..46e3d10d0e8 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_rule_based/otel-config.yaml @@ -0,0 +1,34 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-sampler-composite-rule-based +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces + sampler: + composite/development: + rule_based: + rules: + - attribute_values: + key: route + values: [/health] + sampler: + always_off: + - attribute_patterns: + key: route + included: [/api/*] + excluded: [/api/internal*] + sampler: + always_on: + - span_kinds: [server] + sampler: + always_on: + - parent: [local] + sampler: + always_on: + - sampler: + always_off: diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_rule_based/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_rule_based/run.py new file mode 100644 index 00000000000..759078fc07e --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_rule_based/run.py @@ -0,0 +1,19 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace +from opentelemetry.trace import SpanKind + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("health", kind=SpanKind.SERVER, attributes={"route": "/health"}): + pass +with tracer.start_as_current_span("api", attributes={"route": "/api/users"}): + with tracer.start_as_current_span("child"): + pass +with tracer.start_as_current_span("api-internal", attributes={"route": "/api/internal/x"}): + pass +with tracer.start_as_current_span("server", kind=SpanKind.SERVER): + pass +with tracer.start_as_current_span("other"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_rule_based/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_rule_based/scenario.yaml new file mode 100644 index 00000000000..37124597ee0 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_composite_rule_based/scenario.yaml @@ -0,0 +1,25 @@ +description: > + `composite/development` with `rule_based` rules on attribute values, attribute patterns, + span kinds and parent, falling back to `always_off`. +until: + - exited: {} +expect: + - spans: + name: health + count: 0 + - spans: + name: api + count: 1 + - spans: + name: api-internal + count: 0 + - spans: + name: server + count: 1 + - spans: + name: other + count: 0 + - spans: + name: child + parent: {name: api} + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_parent_based/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_parent_based/otel-config.yaml new file mode 100644 index 00000000000..713e7f02f34 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_parent_based/otel-config.yaml @@ -0,0 +1,23 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-sampler-parent-based +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces + sampler: + parent_based: + root: + always_on: + remote_parent_sampled: + always_off: + remote_parent_not_sampled: + always_on: + local_parent_sampled: + always_off: + local_parent_not_sampled: + always_on: diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_parent_based/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_parent_based/run.py new file mode 100644 index 00000000000..a33896dd261 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_parent_based/run.py @@ -0,0 +1,30 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace +from opentelemetry.trace import NonRecordingSpan, SpanContext, TraceFlags + +tracer = trace.get_tracer(__name__) + + +def parent_context(sampled: bool, remote: bool): + span_context = SpanContext( + trace_id=0x0AF7651916CD43DD8448EB211C80319C, + span_id=0xB7AD6B7169203331, + is_remote=remote, + trace_flags=TraceFlags(TraceFlags.SAMPLED if sampled else TraceFlags.DEFAULT), + ) + return trace.set_span_in_context(NonRecordingSpan(span_context)) + + +with tracer.start_as_current_span("root"): + with tracer.start_as_current_span("local-sampled-child"): + pass + +for name, sampled, remote in [ + ("local-unsampled-child", False, False), + ("remote-sampled-child", True, True), + ("remote-unsampled-child", False, True), +]: + with tracer.start_as_current_span(name, context=parent_context(sampled, remote)): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_parent_based/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_parent_based/scenario.yaml new file mode 100644 index 00000000000..10514f7e462 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_parent_based/scenario.yaml @@ -0,0 +1,21 @@ +description: > + `parent_based` applies each configured sub-sampler; every one is the opposite of its default. +until: + - exited: {} +expect: + - spans: + name: root + root: true + count: 1 + - spans: + name: local-sampled-child + count: 0 + - spans: + name: local-unsampled-child + count: 1 + - spans: + name: remote-sampled-child + count: 0 + - spans: + name: remote-unsampled-child + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_trace_id_ratio/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_trace_id_ratio/otel-config.yaml new file mode 100644 index 00000000000..ffef8501164 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_trace_id_ratio/otel-config.yaml @@ -0,0 +1,14 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-sampler-trace-id-ratio +tracer_provider: + processors: + - batch: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces + sampler: + trace_id_ratio_based: + ratio: 0.5 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_trace_id_ratio/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_trace_id_ratio/run.py new file mode 100644 index 00000000000..27605cb9f71 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_trace_id_ratio/run.py @@ -0,0 +1,10 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +for _ in range(200): + with tracer.start_as_current_span("ratio-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_trace_id_ratio/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_trace_id_ratio/scenario.yaml new file mode 100644 index 00000000000..62bb1bd5634 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_sampler_trace_id_ratio/scenario.yaml @@ -0,0 +1,9 @@ +description: > + `trace_id_ratio_based` with ratio 0.5 samples about half of 200 root spans. +until: + - exited: {} +expect: + - spans: + name: ratio-span + min_count: 60 + max_count: 140 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_simple_otlp_http/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_simple_otlp_http/otel-config.yaml new file mode 100644 index 00000000000..02d5ba40d23 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_simple_otlp_http/otel-config.yaml @@ -0,0 +1,11 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-simple-otlp-http +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_simple_otlp_http/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_simple_otlp_http/run.py new file mode 100644 index 00000000000..cac0e497404 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_simple_otlp_http/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("simple-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_simple_otlp_http/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_simple_otlp_http/scenario.yaml new file mode 100644 index 00000000000..4b17a73d348 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_simple_otlp_http/scenario.yaml @@ -0,0 +1,9 @@ +description: > + Exports a span over OTLP/HTTP through a simple span processor. +until: + - spans: + name: simple-span +expect: + - spans: + name: simple-span + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_span_limits/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_span_limits/otel-config.yaml new file mode 100644 index 00000000000..f3bd2145d91 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_span_limits/otel-config.yaml @@ -0,0 +1,19 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-span-limits +attribute_limits: + attribute_count_limit: 10 + attribute_value_length_limit: 100 +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces + limits: + attribute_count_limit: 2 + attribute_value_length_limit: 4 + link_count_limit: 1 + link_attribute_count_limit: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_span_limits/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_span_limits/run.py new file mode 100644 index 00000000000..ba8a7dfa6b6 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_span_limits/run.py @@ -0,0 +1,18 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace +from opentelemetry.trace import Link + +tracer = trace.get_tracer(__name__) + +with tracer.start_as_current_span("target") as target: + pass + +# Once a count limit is reached, the oldest attribute or link is dropped. +links = [ + Link(target.get_span_context(), {"z": 3}), + Link(target.get_span_context(), {"x": 1, "y": 2}), +] +with tracer.start_as_current_span("limited-span", attributes={"dropped": 0, "a": "abcdefgh", "b": 1}, links=links): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_span_limits/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_span_limits/scenario.yaml new file mode 100644 index 00000000000..3c97114613a --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_span_limits/scenario.yaml @@ -0,0 +1,22 @@ +description: > + Span `limits` cap attribute count and length, link count and link attribute count, overriding + the global `attribute_limits`. +until: + - spans: + name: limited-span +expect: + - spans: + name: limited-span + attributes: + values: + a: abcd + b: 1 + strict: true + link_count: 1 + links: + - span: {name: target} + attributes: + values: + y: 2 + strict: true + count: 1 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_tracer_configurator/otel-config.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_tracer_configurator/otel-config.yaml new file mode 100644 index 00000000000..e8dd16ad15a --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_tracer_configurator/otel-config.yaml @@ -0,0 +1,18 @@ +file_format: "1.0" +resource: + attributes: + - name: service.name + value: declconfig-trace-tracer-configurator +tracer_provider: + processors: + - simple: + exporter: + otlp_http: + endpoint: http://${OTEL_COLLECTOR_ENDPOINT}:4318/v1/traces + tracer_configurator/development: + default_config: + enabled: true + tracers: + - name: disabled.* + config: + enabled: false diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_tracer_configurator/run.py b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_tracer_configurator/run.py new file mode 100644 index 00000000000..26864c3d55c --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_tracer_configurator/run.py @@ -0,0 +1,9 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from opentelemetry import trace + +with trace.get_tracer("enabled.lib").start_as_current_span("enabled-span"): + pass +with trace.get_tracer("disabled.lib").start_as_current_span("disabled-span"): + pass diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_tracer_configurator/scenario.yaml b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_tracer_configurator/scenario.yaml new file mode 100644 index 00000000000..1cf9add1fa1 --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/scenarios/trace_tracer_configurator/scenario.yaml @@ -0,0 +1,12 @@ +description: > + `tracer_configurator/development` disables tracers whose name matches a glob. +until: + - exited: {} +expect: + - spans: + name: enabled-span + scope: {name: enabled.lib} + count: 1 + - spans: + name: disabled-span + count: 0 diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/test_scenario_schema.py b/tests/opentelemetry-docker-tests/tests/declconfig/test_scenario_schema.py new file mode 100644 index 00000000000..89ab0909ecf --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/test_scenario_schema.py @@ -0,0 +1,798 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import json +import re +from pathlib import Path +from typing import Any +from unittest import TestCase + +import jsonschema + +from opentelemetry.proto.common.v1.common_pb2 import ( + AnyValue, + ArrayValue, + InstrumentationScope, + KeyValue, + KeyValueList, +) +from opentelemetry.proto.logs.v1.logs_pb2 import LogRecord, SeverityNumber +from opentelemetry.proto.metrics.v1.metrics_pb2 import ( + AggregationTemporality, + Exemplar, + Gauge, + Histogram, + HistogramDataPoint, + Metric, + NumberDataPoint, + Sum, +) +from opentelemetry.proto.resource.v1.resource_pb2 import Resource +from opentelemetry.proto.trace.v1.trace_pb2 import Span, Status +from opentelemetry.test._otlp_test_server import ( + RecordedLogRecord, + RecordedMetric, + RecordedSpan, +) + +from ._expect import ( + AttributesMatcher, + Counts, + DataPointMatcher, + LinkMatcher, + LiteralMatcher, + LogMatcher, + LogsExpectation, + MetricExpectation, + MetricMatcher, + Mismatch, + ResourceExpectation, + ScopeMatcher, + SpanMatcher, + SpansExpectation, + ValueMatcher, + _describe_value, + parse_value_matcher, +) +from ._scenario import ( + _SCHEMA_FILE, + ExitedCondition, + LogsCondition, + MetricCondition, + Received, + SpansCondition, + load_scenario, + parse_scenario, +) + +_SCENARIOS_DIR = Path(__file__).parent / "scenarios" +_TRACE_ID = b"\x0a" * 16 +_OUTER_SPAN_ID = b"\x01" * 8 +_INNER_SPAN_ID = b"\x02" * 8 + + +def _build_attributes(**attributes: AnyValue) -> list[KeyValue]: + return [KeyValue(key=key, value=value) for key, value in attributes.items()] + + +def _build_span( + name: str, + resource: Resource | None = None, + scope: InstrumentationScope | None = None, + **span_fields: Any, +) -> RecordedSpan: + return RecordedSpan( + span=Span(name=name, **span_fields), + resource=resource or Resource(), + scope=scope or InstrumentationScope(), + ) + + +def _build_metric( + metric: Metric, resource: Resource | None = None, scope: InstrumentationScope | None = None +) -> RecordedMetric: + return RecordedMetric(metric=metric, resource=resource or Resource(), scope=scope or InstrumentationScope()) + + +def _build_log( + body: str, + resource: Resource | None = None, + scope: InstrumentationScope | None = None, + **log_fields: Any, +) -> RecordedLogRecord: + return RecordedLogRecord( + log_record=LogRecord(body=AnyValue(string_value=body), **log_fields), + resource=resource or Resource(), + scope=scope or InstrumentationScope(), + ) + + +def _build_scenario_doc(**overrides: Any) -> dict[str, Any]: + return {"description": "test", "until": [{"spans": {}}], **overrides} + + +def _get_failures(expect: list[dict[str, Any]], received: Received) -> list[str]: + failures = parse_scenario(_build_scenario_doc(expect=expect)).failed_expectations(received) + return [str(failure) for failure in failures] + + +class TestScenarioSchema(TestCase): + def test_bundled_scenarios_load(self): + for path in sorted(_SCENARIOS_DIR.glob("*/scenario.yaml")): + with self.subTest(scenario=path.parent.name): + load_scenario(path) + + def test_defaults(self): + scenario = parse_scenario( + _build_scenario_doc(until=[{"spans": {}}, {"metric": {"name": "m", "min": 1}}, {"logs": {}}]) + ) + self.assertEqual(scenario.timeout, 30.0) + self.assertEqual(scenario.env, {}) + self.assertEqual( + scenario.until, + [SpansCondition(), MetricCondition(name="m", min=1), LogsCondition()], + ) + + def test_parses_conditions(self): + scenario = parse_scenario( + _build_scenario_doc( + timeout=5, + env={"NAME": "value"}, + until=[ + {"spans": {"min_count": 2, "name": "s"}}, + {"metric": {"name": "m", "field": "count", "min": 3}}, + {"logs": {"min_count": 4, "body_pattern": "a.*b"}}, + {"exited": {}}, + ], + ) + ) + self.assertEqual(scenario.timeout, 5.0) + self.assertEqual(scenario.env, {"NAME": "value"}) + self.assertEqual( + scenario.until, + [ + SpansCondition(min_count=2, name="s"), + MetricCondition(name="m", min=3, field="count"), + LogsCondition(min_count=4, body_pattern=re.compile("a.*b")), + ExitedCondition(), + ], + ) + + def test_schema_is_valid(self): + with open(_SCHEMA_FILE, encoding="utf-8") as schema_file: + jsonschema.Draft202012Validator.check_schema(json.load(schema_file)) + + def test_parses_expectations(self): + scenario = parse_scenario( + _build_scenario_doc( + expect=[ + {"resource": {"attributes": {"values": {"service.name": "svc"}, "strict": True}}}, + { + "spans": { + "name": {"pattern": "^GET "}, + "kind": "SERVER", + "status": {"code": "ERROR", "message": "boom"}, + "parent": {"name": "outer", "root": True}, + "links": [ + {"trace_state": "k=v", "span": {"name": "producer"}, "attributes": {"values": {"n": 1}}} + ], + "scope": {"name": "lib", "version": {"exists": True}}, + "count": 2, + } + }, + { + "metric": { + "name": "requests", + "type": "sum", + "temporality": "CUMULATIVE", + "monotonic": True, + "data_point": { + "attributes": {"values": {"route": "/a"}}, + "value": {"gte": 3, "lt": 10}, + "explicit_bounds": [0, 5], + }, + "max_count": 4, + } + }, + { + "logs": { + "body": {"pattern": "user .* in"}, + "severity_number": "WARN", + "event_name": "login", + "trace_context": True, + "span": {"name": "outer"}, + "attributes": {"values": {"absent": {"exists": False}, "ratio": {"double": 1}}}, + "count": 0, + } + }, + ] + ) + ) + self.assertEqual( + scenario.expect, + [ + ResourceExpectation( + attributes=AttributesMatcher(matchers={"service.name": ValueMatcher(equals="svc")}, strict=True) + ), + SpansExpectation( + matcher=SpanMatcher( + name=ValueMatcher(pattern=re.compile("^GET ")), + kind=LiteralMatcher("SERVER"), + status_code=LiteralMatcher("ERROR"), + status_message=ValueMatcher(equals="boom"), + parent=SpanMatcher(name=ValueMatcher(equals="outer"), root=LiteralMatcher(True)), + links=( + LinkMatcher( + trace_state=ValueMatcher(equals="k=v"), + span=SpanMatcher(name=ValueMatcher(equals="producer")), + attributes=AttributesMatcher(matchers={"n": ValueMatcher(equals=1)}), + ), + ), + scope=ScopeMatcher(name=ValueMatcher(equals="lib"), version=ValueMatcher(exists=True)), + ), + counts=Counts(count=2), + ), + MetricExpectation( + matcher=MetricMatcher( + name=ValueMatcher(equals="requests"), + type=LiteralMatcher("sum"), + temporality=LiteralMatcher("CUMULATIVE"), + monotonic=LiteralMatcher(True), + data_point=DataPointMatcher( + value=ValueMatcher(bounds=(("gte", 3), ("lt", 10))), + explicit_bounds=LiteralMatcher([0, 5]), + attributes=AttributesMatcher(matchers={"route": ValueMatcher(equals="/a")}), + ), + ), + counts=Counts(max_count=4), + ), + LogsExpectation( + matcher=LogMatcher( + body=ValueMatcher(pattern=re.compile("user .* in")), + severity_number=LiteralMatcher("WARN"), + event_name=ValueMatcher(equals="login"), + trace_context=LiteralMatcher(True), + span=SpanMatcher(name=ValueMatcher(equals="outer")), + attributes=AttributesMatcher( + matchers={ + "absent": ValueMatcher(exists=False), + "ratio": ValueMatcher(equals=1.0), + } + ), + ), + counts=Counts(count=0), + ), + ], + ) + + def test_invalid_documents(self): + cases = { + "missing description": {"until": [{"spans": {}}]}, + "missing until": {"description": "test"}, + "empty until": _build_scenario_doc(until=[]), + "unknown top-level key": _build_scenario_doc(extra=1), + "non-positive timeout": _build_scenario_doc(timeout=0), + "unknown signal": _build_scenario_doc(until=[{"traces": {}}]), + "two signals in one item": _build_scenario_doc(until=[{"spans": {}, "logs": {}}]), + "unknown condition key": _build_scenario_doc(until=[{"spans": {"count": 1}}]), + "zero min_count": _build_scenario_doc(until=[{"spans": {"min_count": 0}}]), + "metric missing min": _build_scenario_doc(until=[{"metric": {"name": "m"}}]), + "bad metric field": _build_scenario_doc(until=[{"metric": {"name": "m", "field": "max", "min": 1}}]), + "unknown expect signal": _build_scenario_doc(expect=[{"traces": {}}]), + "two signals in one expect item": _build_scenario_doc(expect=[{"spans": {}, "logs": {}}]), + "unknown span field": _build_scenario_doc(expect=[{"spans": {"events": []}}]), + "count with min_count": _build_scenario_doc(expect=[{"spans": {"count": 1, "min_count": 1}}]), + "count with max_count": _build_scenario_doc(expect=[{"logs": {"count": 1, "max_count": 1}}]), + "zero max_count": _build_scenario_doc(expect=[{"logs": {"max_count": 0}}]), + "count on nested span matcher": _build_scenario_doc(expect=[{"spans": {"parent": {"count": 1}}}]), + "root with parent": _build_scenario_doc(expect=[{"spans": {"root": True, "parent": {}}}]), + "bad span kind": _build_scenario_doc(expect=[{"spans": {"kind": "server"}}]), + "bad status code": _build_scenario_doc(expect=[{"spans": {"status": {"code": "FAILED"}}}]), + "bad severity": _build_scenario_doc(expect=[{"logs": {"severity_number": "WARNING"}}]), + "bad temporality": _build_scenario_doc(expect=[{"metric": {"temporality": "cumulative"}}]), + "bad metric type": _build_scenario_doc(expect=[{"metric": {"type": "counter"}}]), + "unknown data point field": _build_scenario_doc(expect=[{"metric": {"data_point": {"buckets": []}}}]), + "string data point value": _build_scenario_doc(expect=[{"metric": {"data_point": {"value": "3"}}}]), + "pattern on data point value": _build_scenario_doc( + expect=[{"metric": {"data_point": {"value": {"pattern": "3"}}}}] + ), + "exists false with other operators": _build_scenario_doc( + expect=[{"spans": {"attributes": {"values": {"a": {"exists": False, "pattern": "x"}}}}}] + ), + "typed literal with two types": _build_scenario_doc( + expect=[{"spans": {"attributes": {"values": {"a": {"int": 1, "double": 1.0}}}}}] + ), + "typed literal with operator": _build_scenario_doc( + expect=[{"spans": {"attributes": {"values": {"a": {"int": 1, "gt": 0}}}}}] + ), + "wrongly typed literal": _build_scenario_doc( + expect=[{"spans": {"attributes": {"values": {"a": {"int": "1"}}}}}] + ), + "empty operators": _build_scenario_doc(expect=[{"spans": {"attributes": {"values": {"a": {}}}}}]), + "empty resource": _build_scenario_doc(expect=[{"resource": {}}]), + "resource with count": _build_scenario_doc( + expect=[{"resource": {"attributes": {"values": {}}, "count": 1}}] + ), + "empty attributes": _build_scenario_doc(expect=[{"spans": {"attributes": {}}}]), + "unwrapped attributes": _build_scenario_doc(expect=[{"spans": {"attributes": {"a": 1}}}]), + "unknown scope field": _build_scenario_doc(expect=[{"spans": {"scope": {"schema_url": "x"}}}]), + "non-string env value": _build_scenario_doc(env={"N": 1}), + "exited with fields": _build_scenario_doc(until=[{"exited": {"code": 0}}]), + "output without pattern": _build_scenario_doc(expect=[{"output": {"stream": "stdout"}}]), + "bad output stream": _build_scenario_doc(expect=[{"output": {"stream": "stdin", "pattern": "x"}}]), + "string link_count": _build_scenario_doc(expect=[{"spans": {"link_count": "1"}}]), + "pattern on exemplar_count": _build_scenario_doc( + expect=[{"metric": {"data_point": {"exemplar_count": {"pattern": "1"}}}}] + ), + } + for case, document in cases.items(): + with self.subTest(case=case), self.assertRaises(jsonschema.ValidationError): + parse_scenario(document) + + def test_invalid_body_pattern(self): + with self.assertRaises(re.error): + parse_scenario(_build_scenario_doc(until=[{"logs": {"body_pattern": "("}}])) + + def test_invalid_expect_pattern(self): + with self.assertRaises(re.error): + parse_scenario(_build_scenario_doc(expect=[{"spans": {"name": {"pattern": "("}}}])) + + +class TestConditions(TestCase): + def test_spans(self): + received = Received(spans=[_build_span("a"), _build_span("b"), _build_span("a")]) + cases = [ + (SpansCondition(min_count=3), True, "spans(name=None): 3/3"), + (SpansCondition(min_count=4), False, "spans(name=None): 3/4"), + (SpansCondition(min_count=2, name="a"), True, "spans(name='a'): 2/2"), + (SpansCondition(name="c"), False, "spans(name='c'): 0/1"), + ] + for condition, met, progress in cases: + with self.subTest(condition=condition): + self.assertEqual(condition.satisfied(received), met) + self.assertEqual(condition.progress(received), progress) + + def test_metrics(self): + counter = Metric( + name="counter", + sum=Sum(data_points=[NumberDataPoint(as_int=2), NumberDataPoint(as_int=5)]), + ) + gauge = Metric(name="gauge", gauge=Gauge(data_points=[NumberDataPoint(as_double=1.5)])) + histogram = Metric( + name="histogram", + histogram=Histogram(data_points=[HistogramDataPoint(count=4, sum=10.0)]), + ) + received = Received(metrics=[_build_metric(counter), _build_metric(gauge), _build_metric(histogram)]) + cases = [ + (MetricCondition(name="counter", min=5), True), + (MetricCondition(name="counter", min=6), False), + (MetricCondition(name="gauge", min=1.5), True), + (MetricCondition(name="histogram", field="count", min=4), True), + (MetricCondition(name="histogram", field="sum", min=10.5), False), + (MetricCondition(name="missing", min=0), False), + ] + for condition, met in cases: + with self.subTest(condition=condition): + self.assertEqual(condition.satisfied(received), met) + + self.assertEqual( + MetricCondition(name="missing", min=0).progress(received), + "metric(name='missing', field='value'): max None (need >= 0)", + ) + + def test_metric_field_mismatch(self): + received = Received( + metrics=[_build_metric(Metric(name="gauge", gauge=Gauge(data_points=[NumberDataPoint(as_int=1)])))] + ) + cases = [ + MetricCondition(name="gauge", field="count", min=1), + MetricCondition(name="gauge", field="sum", min=1), + ] + for condition in cases: + with self.subTest(condition=condition), self.assertRaises(ValueError): + condition.satisfied(received) + + def test_logs(self): + received = Received( + logs=[_build_log("user alice logged in"), _build_log("startup"), _build_log("user bob logged in")] + ) + cases = [ + (LogsCondition(min_count=3), True, "logs(body_pattern=None): 3/3"), + ( + LogsCondition(min_count=2, body_pattern=re.compile(r"user .* in")), + True, + "logs(body_pattern='user .* in'): 2/2", + ), + (LogsCondition(body_pattern=re.compile("shutdown")), False, "logs(body_pattern='shutdown'): 0/1"), + ] + for condition, met, progress in cases: + with self.subTest(condition=condition): + self.assertEqual(condition.satisfied(received), met) + self.assertEqual(condition.progress(received), progress) + + def test_exited(self): + cases = [(Received(), False, "exited: False"), (Received(exited=True), True, "exited: True")] + for received, met, progress in cases: + with self.subTest(exited=received.exited): + self.assertEqual(ExitedCondition().satisfied(received), met) + self.assertEqual(ExitedCondition().progress(received), progress) + + +class TestExpectations(TestCase): + def test_mismatch_str(self): + cases = [ + (Mismatch(("name",), "a", "b"), "name: expected a, got b"), + (Mismatch(("attributes", "['k']"), "1", ""), "attributes['k']: expected 1, got "), + (Mismatch(("value",), "3", "4").nested("data_point"), "data_point.value: expected 3, got 4"), + ( + Mismatch(("name",), "a span", "").nested("span").nested("links[0]"), + "links[0].span.name: expected a span, got ", + ), + ] + for mismatch, text in cases: + with self.subTest(mismatch=mismatch): + self.assertEqual(str(mismatch), text) + + def test_value_matchers(self): + int_value = AnyValue(int_value=5) + double_value = AnyValue(double_value=5.0) + string_value = AnyValue(string_value="abc") + array_value = AnyValue(array_value=ArrayValue(values=[AnyValue(int_value=1), AnyValue(string_value="a")])) + kvlist_value = AnyValue( + kvlist_value=KeyValueList(values=_build_attributes(b=AnyValue(bool_value=True), a=AnyValue(int_value=1))) + ) + cases = [ + (5, int_value, True), + (5, double_value, False), + (5.0, double_value, True), + (5.0, int_value, False), + ("5", int_value, False), + ({"double": 5}, double_value, True), + ({"int": 5}, double_value, False), + ({"string": "abc"}, string_value, True), + (True, AnyValue(bool_value=True), True), + (True, AnyValue(int_value=1), False), + (1, AnyValue(bool_value=True), False), + ([1, "a"], array_value, True), + ([1.0, "a"], array_value, False), + (["a", 1], array_value, False), + ({"kvlist": {"a": 1, "b": True}}, kvlist_value, True), + ({"kvlist": {"a": 1}}, kvlist_value, False), + ({"kvlist": {"a": True, "b": True}}, kvlist_value, False), + ( + [1, "a"], + AnyValue(array_value=ArrayValue(values=[AnyValue(bool_value=True), AnyValue(string_value="a")])), + False, + ), + ({"pattern": "^ab"}, string_value, True), + ({"pattern": "^b"}, string_value, False), + ({"pattern": "5"}, int_value, False), + ({"gte": 5, "lt": 6}, int_value, True), + ({"gte": 5, "lt": 6}, double_value, True), + ({"gt": 5}, int_value, False), + ({"gt": 0}, string_value, False), + ({"exists": True, "pattern": "c$"}, string_value, True), + ({"exists": True}, None, False), + ({"exists": False}, None, True), + ({"exists": False}, int_value, False), + (5, None, False), + ] + for spec, value, matches in cases: + with self.subTest(spec=spec, value=value): + attributes = _build_attributes(key=value) if value is not None else [] + matcher = AttributesMatcher(matchers={"key": parse_value_matcher(spec)}) + self.assertEqual(not list(matcher.check("attributes", attributes)), matches) + + def test_describe_value(self): + cases = [ + (5, "5"), + (5.0, "5.0"), + ("5", "'5'"), + ("true", "'true'"), + ("", "''"), + ("abc", "abc"), + (True, "true"), + ([1, "a"], "[1, a]"), + ({"a": 1, "b": [2.5]}, "{a: 1, b: [2.5]}"), + (None, ""), + ] + for value, text in cases: + with self.subTest(value=value): + self.assertEqual(_describe_value(value), text) + + def test_attributes_strict(self): + attributes = _build_attributes(a=AnyValue(int_value=1), b=AnyValue(int_value=2)) + cases = [ + ({"attributes": {"values": {"a": 1}}}, []), + ({"attributes": {"values": {"a": 1}, "strict": True}}, ["attributes: expected no unlisted keys, got b"]), + ({"attributes": {"values": {"a": 1, "b": 2, "c": {"exists": False}}, "strict": True}}, []), + ({"attributes": {"strict": True}}, ["attributes: expected no unlisted keys, got a, b"]), + ] + received = Received(spans=[_build_span("s", attributes=attributes)]) + for spec, problems in cases: + with self.subTest(spec=spec): + failures = _get_failures([{"spans": spec}], received) + self.assertEqual(failures != [], bool(problems)) + for problem in problems: + self.assertIn(problem, failures[0]) + + def test_resource(self): + service = Resource(attributes=_build_attributes(**{"service.name": AnyValue(string_value="svc")})) + other = Resource(attributes=_build_attributes(**{"service.name": AnyValue(string_value="other")})) + expect = [{"resource": {"attributes": {"values": {"service.name": "svc"}}}}] + cases = [ + ("all match", Received(spans=[_build_span("s", service)], logs=[_build_log("b", service)]), None), + ( + "one mismatch", + Received(spans=[_build_span("s", service)], logs=[_build_log("b", other), _build_log("c", other)]), + "2 of 3 items have a non-matching resource\n log: attributes['service.name']: expected svc, got other", + ), + ("nothing received", Received(), "nothing received"), + ] + for case, received, problem in cases: + with self.subTest(case=case): + failures = _get_failures(expect, received) + if problem is None: + self.assertEqual(failures, []) + else: + self.assertEqual(len(failures), 1) + self.assertIn(problem, failures[0]) + + def test_resource_schema_url(self): + resource = Resource(attributes=_build_attributes(**{"service.name": AnyValue(string_value="svc")})) + span = _build_span("s", resource) + cases = [ + ({"pattern": "1\\.26"}, "https://opentelemetry.io/schemas/1.26.0", None), + ("https://opentelemetry.io/schemas/1.26.0", "https://opentelemetry.io/schemas/1.27.0", "schema_url"), + ({"exists": False}, "", None), + ({"exists": True}, "", "schema_url: expected present, got "), + ] + for spec, schema_url, problem in cases: + with self.subTest(spec=spec, schema_url=schema_url): + span.resource_schema_url = schema_url + failures = _get_failures([{"resource": {"schema_url": spec}}], Received(spans=[span])) + if problem is None: + self.assertEqual(failures, []) + else: + self.assertEqual(len(failures), 1) + self.assertIn(problem, failures[0]) + + def test_output(self): + received = Received(stdout='{"name": "console-span"}', stderr="Failed to auto initialize OpenTelemetry") + cases = [ + ({"stream": "stdout", "pattern": '"name": "console-span"'}, None), + ({"stream": "stderr", "pattern": "console-span"}, "stderr has no match for 'console-span'"), + ( + {"stream": "stderr", "pattern": "Failed to auto", "exists": False}, + "stderr has a match for 'Failed to auto'", + ), + ({"stream": "stdout", "pattern": "Traceback", "exists": False}, None), + ] + for spec, problem in cases: + with self.subTest(spec=spec): + failures = _get_failures([{"output": spec}], received) + if problem is None: + self.assertEqual(failures, []) + else: + self.assertEqual(len(failures), 1) + self.assertIn(problem, failures[0]) + + def test_link_count(self): + target = _build_span("target", trace_id=_TRACE_ID, span_id=_OUTER_SPAN_ID) + link = Span.Link(trace_id=_TRACE_ID, span_id=_OUTER_SPAN_ID) + received = Received(spans=[target, _build_span("linked", links=[link, link])]) + cases = [ + ({"link_count": 2}, True), + ({"link_count": 1}, False), + ({"link_count": {"gte": 1, "lt": 3}}, True), + ({"name": "target", "link_count": 0}, True), + ] + for spec, met in cases: + with self.subTest(spec=spec): + spec = {"name": "linked", **spec} + self.assertEqual(_get_failures([{"spans": spec}], received) == [], met) + + def test_exemplar_count(self): + sum_metric = Metric( + name="requests", + sum=Sum(data_points=[NumberDataPoint(as_int=1, exemplars=[Exemplar(as_int=1), Exemplar(as_int=1)])]), + ) + received = Received(metrics=[_build_metric(sum_metric)]) + cases = [ + ({"exemplar_count": 2}, True), + ({"exemplar_count": {"gte": 1}}, True), + ({"exemplar_count": 0}, False), + ] + for data_point, met in cases: + with self.subTest(data_point=data_point): + expect = [{"metric": {"name": "requests", "data_point": data_point}}] + self.assertEqual(_get_failures(expect, received) == [], met) + + def test_spans(self): + outer = _build_span( + "outer", + scope=InstrumentationScope(name="lib", version="1.0"), + trace_id=_TRACE_ID, + span_id=_OUTER_SPAN_ID, + ) + inner = _build_span( + "GET /a", + trace_id=_TRACE_ID, + span_id=_INNER_SPAN_ID, + parent_span_id=_OUTER_SPAN_ID, + kind=Span.SPAN_KIND_SERVER, + status=Status(code=Status.STATUS_CODE_ERROR, message="boom"), + trace_state="k=v", + links=[ + Span.Link( + trace_id=_TRACE_ID, + span_id=_OUTER_SPAN_ID, + attributes=_build_attributes(n=AnyValue(int_value=1)), + ) + ], + attributes=_build_attributes(**{"http.method": AnyValue(string_value="GET")}), + ) + orphan = _build_span("orphan", trace_id=_TRACE_ID, span_id=b"\x03" * 8, parent_span_id=b"\x09" * 8) + received = Received(spans=[outer, inner, orphan]) + cases = [ + ({}, True), + ({"count": 3}, True), + ({"max_count": 2}, False), + ({"name": {"pattern": "^GET "}, "kind": "SERVER"}, True), + ({"name": "GET /a", "kind": "INTERNAL"}, False), + ({"status": {"code": "ERROR", "message": {"pattern": "boo"}}}, True), + ({"status": {"code": "OK"}}, False), + ({"trace_state": "k=v"}, True), + ({"root": True, "count": 1}, True), + ({"root": False, "count": 2}, True), + ({"name": "GET /a", "parent": {"name": "outer", "root": True}}, True), + ({"name": "GET /a", "parent": {"name": "other"}}, False), + ({"name": "orphan", "parent": {}}, False), + ({"links": [{"attributes": {"values": {"n": 1}}, "span": {"name": "outer"}}]}, True), + ({"links": [{"attributes": {"values": {"n": 2}}}]}, False), + ({"links": [{"trace_state": {"exists": True}}]}, False), + ({"name": "outer", "scope": {"name": "lib", "version": "1.0"}}, True), + ({"name": "outer", "scope": {"version": {"exists": False}}}, False), + ({"attributes": {"values": {"http.method": "GET"}}, "count": 1}, True), + ({"name": "missing", "count": 0}, True), + ({"name": "outer", "count": 0}, False), + ] + for spec, met in cases: + with self.subTest(spec=spec): + self.assertEqual(_get_failures([{"spans": spec}], received) == [], met) + + def test_metrics(self): + def build_sum(value: int, route: str) -> Metric: + return Metric( + name="requests", + unit="1", + sum=Sum( + aggregation_temporality=AggregationTemporality.AGGREGATION_TEMPORALITY_CUMULATIVE, + is_monotonic=True, + data_points=[ + NumberDataPoint(as_int=value, attributes=_build_attributes(route=AnyValue(string_value=route))) + ], + ), + ) + + histogram = Metric( + name="duration", + histogram=Histogram( + aggregation_temporality=AggregationTemporality.AGGREGATION_TEMPORALITY_DELTA, + data_points=[ + HistogramDataPoint(count=4, sum=10.0, min=1.0, explicit_bounds=[0, 5], bucket_counts=[0, 3, 1]) + ], + ), + ) + gauge = Metric(name="temperature", gauge=Gauge(data_points=[NumberDataPoint(as_double=21.5)])) + received = Received( + metrics=[ + _build_metric(build_sum(1, "/a"), scope=InstrumentationScope(name="app")), + _build_metric(build_sum(3, "/a")), + _build_metric(histogram), + _build_metric(gauge), + ] + ) + cases = [ + ({"name": "requests", "count": 2}, True), + ({"name": "requests", "data_point": {"value": {"gte": 3}}, "count": 1}, True), + ({"name": "requests", "data_point": {"value": 3}}, True), + ({"name": "requests", "data_point": {"value": 3.0}}, False), + ({"name": "requests", "data_point": {"attributes": {"values": {"route": "/b"}}}, "count": 0}, True), + ({"name": "requests", "type": "sum", "unit": "1", "temporality": "CUMULATIVE", "monotonic": True}, True), + ({"name": "requests", "temporality": "DELTA"}, False), + ({"name": "requests", "scope": {"name": "app"}, "count": 1}, True), + ({"name": "requests", "type": "gauge"}, False), + ({"name": "requests", "description": {"exists": False}}, True), + ({"name": "duration", "temporality": "DELTA", "data_point": {"count": 4, "sum": {"gt": 9}}}, True), + ({"name": "duration", "data_point": {"sum": 10, "min": 1, "max": {"exists": False}}}, True), + ({"name": "duration", "data_point": {"max": {"exists": True}}}, False), + ({"name": "duration", "data_point": {"explicit_bounds": [0, 5], "bucket_counts": [0, 3, 1]}}, True), + ({"name": "duration", "data_point": {"explicit_bounds": [0, 10]}}, False), + ({"name": "temperature", "data_point": {"value": 21.5}}, True), + ({"type": "gauge", "data_point": {"value": {"lt": 20}}}, False), + ] + for spec, met in cases: + with self.subTest(spec=spec): + self.assertEqual(_get_failures([{"metric": spec}], received) == [], met) + + def test_metric_field_mismatch(self): + gauge = Metric(name="gauge", gauge=Gauge(data_points=[NumberDataPoint(as_int=1)])) + histogram = Metric(name="histogram", histogram=Histogram(data_points=[HistogramDataPoint(count=1)])) + received = Received(metrics=[_build_metric(gauge), _build_metric(histogram)]) + cases = [ + {"name": "gauge", "temporality": "CUMULATIVE"}, + {"name": "gauge", "monotonic": True}, + {"name": "gauge", "data_point": {"count": 1}}, + {"name": "histogram", "data_point": {"value": 1}}, + {"data_point": {"explicit_bounds": []}}, + ] + for spec in cases: + with self.subTest(spec=spec), self.assertRaises(ValueError): + _get_failures([{"metric": spec}], received) + + def test_logs(self): + span = _build_span("handler", trace_id=_TRACE_ID, span_id=_OUTER_SPAN_ID) + in_span = _build_log( + "user alice logged in", + scope=InstrumentationScope(name="auth"), + trace_id=_TRACE_ID, + span_id=_OUTER_SPAN_ID, + severity_number=SeverityNumber.SEVERITY_NUMBER_WARN, + severity_text="WARN", + event_name="login", + attributes=_build_attributes(user=AnyValue(string_value="alice")), + ) + orphan = _build_log("lost", trace_id=_TRACE_ID, span_id=_INNER_SPAN_ID) + plain = _build_log("startup") + received = Received(spans=[span], logs=[in_span, orphan, plain]) + cases = [ + ({"body": {"pattern": "user .* in"}, "count": 1}, True), + ({"body": "startup"}, True), + ({"body": {"string": "start"}}, False), + ({"severity_number": "WARN", "severity_text": "WARN", "event_name": "login"}, True), + ({"severity_number": "WARN2"}, False), + ({"trace_context": True, "count": 2}, True), + ({"trace_context": False, "count": 1}, True), + ({"span": {"name": "handler"}, "count": 1}, True), + ({"body": "lost", "span": {}}, False), + ({"scope": {"name": "auth"}, "attributes": {"values": {"user": "alice"}}}, True), + ({"event_name": {"exists": True}, "count": 1}, True), + ({"severity_number": "ERROR", "count": 0}, True), + ] + for spec, met in cases: + with self.subTest(spec=spec): + self.assertEqual(_get_failures([{"logs": spec}], received) == [], met) + + def test_failure_messages(self): + received = Received(spans=[_build_span("basic-span"), _build_span("other"), _build_span("other")]) + cases = [ + ( + {"name": "basic-span", "kind": "SERVER"}, + "spans: {name: basic-span, kind: SERVER}\n" + " matched 0, expected >= 1\n" + " closest 'basic-span': kind: expected SERVER, got UNSPECIFIED\n" + " closest 'other': name: expected basic-span, got other; " + "kind: expected SERVER, got UNSPECIFIED", + ), + ( + {"name": "other", "count": 0}, + "spans: {name: other, count: 0}\n matched 2, expected == 0\n matched 'other'\n matched 'other'", + ), + ( + {"kind": "SERVER", "min_count": 2, "max_count": 3}, + "spans: {kind: SERVER, min_count: 2, max_count: 3}\n" + " matched 0, expected >= 2 and <= 3\n" + " closest 'basic-span': kind: expected SERVER, got UNSPECIFIED\n" + " closest 'other': kind: expected SERVER, got UNSPECIFIED", + ), + ] + for spec, message in cases: + with self.subTest(spec=spec): + self.assertEqual(_get_failures([{"spans": spec}], received), [message]) + self.assertEqual( + _get_failures([{"logs": {}}], received), + ["logs: {}\n matched 0, expected >= 1\n no logs received"], + ) diff --git a/tests/opentelemetry-docker-tests/tests/declconfig/test_scenarios.py b/tests/opentelemetry-docker-tests/tests/declconfig/test_scenarios.py new file mode 100644 index 00000000000..53ec03b568f --- /dev/null +++ b/tests/opentelemetry-docker-tests/tests/declconfig/test_scenarios.py @@ -0,0 +1,108 @@ +# Copyright The OpenTelemetry Authors +# SPDX-License-Identifier: Apache-2.0 + +from __future__ import annotations + +import os +import shutil +import signal +import subprocess +import sys +import time +from pathlib import Path + +import pytest + +from opentelemetry.test._otlp_test_server import OtlpProtoTestServer + +from ._scenario import Received, load_scenario + +_SCENARIOS_DIR = Path(__file__).parent / "scenarios" +_SCENARIOS = sorted(path for path in _SCENARIOS_DIR.iterdir() if path.is_dir()) +_POLL_INTERVAL = 0.1 +_STOP_GRACE_PERIOD = 5.0 +# How long the sink must receive nothing after the app stops before `expect` is checked. +_SETTLE_PERIOD = 1.0 +# Upper bound on settling, for sources that never go quiet. +_MAX_SETTLE_DURATION = 10.0 + + +def _stop(process: subprocess.Popen[bytes]) -> None: + """Interrupt the app so atexit (and SDK shutdown) runs, then kill it if it lingers.""" + if process.poll() is not None: + return + process.send_signal(signal.SIGINT) + try: + process.wait(timeout=_STOP_GRACE_PERIOD) + except subprocess.TimeoutExpired: + process.kill() + process.wait() + + +def _get_otel_config(scenario: Path) -> Path: + yaml_config = scenario / "otel-config.yaml" + return yaml_config if yaml_config.exists() else scenario / "otel-config.json" + + +def _drain_until_quiet(received: Received, sink: OtlpProtoTestServer) -> None: + """Collect telemetry still in flight, such as what the SDK flushed on shutdown.""" + deadline = time.monotonic() + _MAX_SETTLE_DURATION + quiet_until = time.monotonic() + _SETTLE_PERIOD + while time.monotonic() < min(quiet_until, deadline): + time.sleep(_POLL_INTERVAL) + if received.drain(sink): + quiet_until = time.monotonic() + _SETTLE_PERIOD + + +@pytest.mark.parametrize("scenario", _SCENARIOS, ids=[path.name for path in _SCENARIOS]) +def test_scenario(scenario: Path, collector: str, otlp_sink: OtlpProtoTestServer, tmp_path: Path) -> None: + spec = load_scenario(scenario / "scenario.yaml") + instrument = shutil.which("opentelemetry-instrument") + assert instrument is not None, "opentelemetry-instrument is not installed" + + stdout_path = tmp_path / "stdout.txt" + stderr_path = tmp_path / "stderr.txt" + + def app_output() -> str: + return f"stdout:\n{stdout_path.read_text()}\nstderr:\n{stderr_path.read_text()}" + + received = Received() + with open(stdout_path, "wb") as stdout, open(stderr_path, "wb") as stderr: + process = subprocess.Popen( + [instrument, sys.executable, "run.py"], + cwd=scenario, + env={ + # Inherited OTEL_* variables would leak into the SDK. + **{key: value for key, value in os.environ.items() if not key.startswith("OTEL_")}, + **spec.env, + "OTEL_CONFIG_FILE": str(_get_otel_config(scenario)), + "OTEL_COLLECTOR_ENDPOINT": collector, + }, + stdout=stdout, + stderr=stderr, + ) + try: + deadline = time.monotonic() + spec.timeout + while time.monotonic() < deadline: + received.drain(otlp_sink) + returncode = process.poll() + assert returncode in (None, 0), f"run.py exited with {returncode}:\n{app_output()}" + received.exited = returncode == 0 + if not spec.unmet(received): + break + time.sleep(_POLL_INTERVAL) + finally: + _stop(process) + + received.stdout = stdout_path.read_text() + received.stderr = stderr_path.read_text() + + _drain_until_quiet(received, otlp_sink) + unmet = spec.unmet(received) + assert not unmet, ( + f"Timed out after {spec.timeout}s waiting for:\n" + + "\n".join(f" {condition.progress(received)}" for condition in unmet) + + f"\n{app_output()}" + ) + failures = spec.failed_expectations(received) + assert not failures, "Unmet expectations:\n" + "\n".join(map(str, failures)) + f"\n{app_output()}" diff --git a/tests/opentelemetry-docker-tests/tests/otlpexporter/conftest.py b/tests/opentelemetry-docker-tests/tests/otlpexporter/conftest.py index d777a33fa6a..17d572ba0f7 100644 --- a/tests/opentelemetry-docker-tests/tests/otlpexporter/conftest.py +++ b/tests/opentelemetry-docker-tests/tests/otlpexporter/conftest.py @@ -6,9 +6,28 @@ from collections.abc import Iterator import pytest +from prometheus_client import CollectorRegistry, start_http_server from opentelemetry.test._otlp_test_server import OtlpProtoTestServer +_PROMETHEUS_PORT = 9464 + + +@pytest.fixture(scope="session") +def prometheus_registry() -> CollectorRegistry: + return CollectorRegistry() + + +@pytest.fixture(scope="session", autouse=True) +def _prometheus_scrape_target(prometheus_registry: CollectorRegistry) -> Iterator[None]: + """Keep a Prometheus HTTP endpoint up on :9464 for the whole docker-test session.""" + httpd, _thread = start_http_server(port=_PROMETHEUS_PORT, addr="0.0.0.0", registry=prometheus_registry) + try: + yield + finally: + httpd.shutdown() + httpd.server_close() + @pytest.fixture(scope="class") def server() -> Iterator[OtlpProtoTestServer]: diff --git a/tests/opentelemetry-test-utils/src/opentelemetry/test/_otlp_test_server.py b/tests/opentelemetry-test-utils/src/opentelemetry/test/_otlp_test_server.py index 553f8506efe..f7c3b053ed3 100644 --- a/tests/opentelemetry-test-utils/src/opentelemetry/test/_otlp_test_server.py +++ b/tests/opentelemetry-test-utils/src/opentelemetry/test/_otlp_test_server.py @@ -26,6 +26,7 @@ class RecordedSpan: span: Span resource: Resource scope: InstrumentationScope + resource_schema_url: str = "" @dataclass @@ -33,6 +34,7 @@ class RecordedMetric: metric: Metric resource: Resource scope: InstrumentationScope + resource_schema_url: str = "" @dataclass @@ -40,6 +42,7 @@ class RecordedLogRecord: log_record: LogRecord resource: Resource scope: InstrumentationScope + resource_schema_url: str = "" def _make_handler( @@ -101,7 +104,14 @@ def _handle_traces(body: bytes) -> bytes: for rs in request.resource_spans: for ss in rs.scope_spans: for span in ss.spans: - spans_queue.put(RecordedSpan(span=span, resource=rs.resource, scope=ss.scope)) + spans_queue.put( + RecordedSpan( + span=span, + resource=rs.resource, + scope=ss.scope, + resource_schema_url=rs.schema_url, + ) + ) return ExportTraceServiceResponse().SerializeToString() @staticmethod @@ -116,6 +126,7 @@ def _handle_metrics(body: bytes) -> bytes: metric=metric, resource=rm.resource, scope=sm.scope, + resource_schema_url=rm.schema_url, ) ) return ExportMetricsServiceResponse().SerializeToString() @@ -132,6 +143,7 @@ def _handle_logs(body: bytes) -> bytes: log_record=log_record, resource=rl.resource, scope=sl.scope, + resource_schema_url=rl.schema_url, ) ) return ExportLogsServiceResponse().SerializeToString() diff --git a/tox.ini b/tox.ini index 7f01c0df478..4729a50c0ee 100644 --- a/tox.ini +++ b/tox.ini @@ -130,6 +130,7 @@ envlist = typecheck docs docker-tests-{otlpexporter,opencensus} + docker-tests-declconfig public-symbols-check shellcheck generate-workflows @@ -220,6 +221,7 @@ setenv = CONTRIB_REPO=git+https://github.com/open-telemetry/opentelemetry-python-contrib.git@{env:CONTRIB_REPO_SHA} CONTRIB_REPO_UTIL_HTTP={env:CONTRIB_REPO_UTIL_HTTP:{env:CONTRIB_REPO}\#egg=opentelemetry-util-http&subdirectory=util/opentelemetry-util-http} CONTRIB_REPO_INSTRUMENTATION={env:CONTRIB_REPO_INSTRUMENTATION:{env:CONTRIB_REPO}\#egg=opentelemetry-instrumentation&subdirectory=opentelemetry-instrumentation} + CONTRIB_REPO_DISTRO={env:CONTRIB_REPO_DISTRO:{env:CONTRIB_REPO}\#egg=opentelemetry-distro&subdirectory=opentelemetry-distro} CONTRIB_REPO_INSTRUMENTATION_REQUESTS={env:CONTRIB_REPO_INSTRUMENTATION_REQUESTS:{env:CONTRIB_REPO}\#egg=opentelemetry-instrumentation-requests&subdirectory=instrumentation/opentelemetry-instrumentation-requests} CONTRIB_REPO_INSTRUMENTATION_WSGI={env:CONTRIB_REPO_INSTRUMENTATION_WSGI:{env:CONTRIB_REPO}\#egg=opentelemetry-instrumentation-wsgi&subdirectory=instrumentation/opentelemetry-instrumentation-wsgi} CONTRIB_REPO_INSTRUMENTATION_FLASK={env:CONTRIB_REPO_INSTRUMENTATION_FLASK:{env:CONTRIB_REPO}\#egg=opentelemetry-instrumentation-flask&subdirectory=instrumentation/opentelemetry-instrumentation-flask} @@ -369,7 +371,7 @@ commands = deps = -c dev-requirements.txt pytest - prometheus_client + otlpexporter: prometheus_client ; core packages -e {toxinidir}/opentelemetry-api -e {toxinidir}/opentelemetry-semantic-conventions @@ -416,6 +418,43 @@ commands = commands_post = docker compose down -v +[testenv:docker-tests-declconfig] +deps = + -c dev-requirements.txt + pytest + jsonschema + pyyaml + testcontainers + ; core packages + -e {toxinidir}/opentelemetry-api + -e {toxinidir}/opentelemetry-semantic-conventions + -e {toxinidir}/opentelemetry-sdk + -e {toxinidir}/opentelemetry-configuration + -e {toxinidir}/tests/opentelemetry-test-utils + ; OTLP packages + -e {toxinidir}/opentelemetry-proto + -e {toxinidir}/opentelemetry-proto-json + -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-common + -e {toxinidir}/exporter/opentelemetry-exporter-otlp-common + -e {toxinidir}/exporter/opentelemetry-exporter-http-transport[requests] + -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http + -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-grpc + -e {toxinidir}/exporter/opentelemetry-exporter-otlp-json-common + -e {toxinidir}/exporter/opentelemetry-exporter-otlp-json-file + -e {toxinidir}/exporter/opentelemetry-exporter-prometheus + ; propagators + -e {toxinidir}/propagator/opentelemetry-propagator-b3 + -e {toxinidir}/propagator/opentelemetry-propagator-jaeger + ; zero-code instrumentation + {env:CONTRIB_REPO_INSTRUMENTATION} + {env:CONTRIB_REPO_DISTRO} + +changedir = + tests/opentelemetry-docker-tests/tests + +commands = + pytest declconfig {posargs} + [testenv:lint-opentelemetry-test-utils] ; This environment must be defined explicitly because of how ; tox parses and matches environment names. @@ -486,6 +525,7 @@ deps = -e {toxinidir}/exporter/opentelemetry-exporter-otlp-proto-http -e {toxinidir}/opentelemetry-proto -e {toxinidir}/opentelemetry-proto-json + -e {toxinidir}/opentelemetry-proto-json -e {toxinidir}/codegen/opentelemetry-codegen-json commands =