-
Notifications
You must be signed in to change notification settings - Fork 103
LCORE-1822: Enable OpenTelemetry delivery E2E test with mock OTLP collector #2609
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,11 +1,10 @@ | ||
| @cfg_authorized @OTel @skip | ||
| @cfg_authorized @OTel @skip-in-prow | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we have a task to get this working also in the konflux environment? If not create one as we need it to work there as well
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mind helping me by creating that? I'm sure you know better than me where exactly the task should be created and tracked and what to write ;)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
| Feature: OpenTelemetry observability tests | ||
|
|
||
| Background: | ||
| Given The service is started locally | ||
| And The system is in default state | ||
| And An OpenTelemetry service is running and listening for OTLP data | ||
| And The service is configured to export data to the OpenTelemetry service | ||
| And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva | ||
| And REST API service prefix is /v1 | ||
| And the Lightspeed stack configuration directory is "tests/e2e/configuration" | ||
|
|
@@ -25,5 +24,4 @@ Feature: OpenTelemetry observability tests | |
| } | ||
| """ | ||
| Then The status code of the response is 200 | ||
| And The service exported an OpenTelemetry event containing e2e-otel-delivery-marker | ||
| And The OpenTelemetry service received data containing e2e-otel-delivery-marker | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| """Step definitions for the OpenTelemetry telemetry-delivery E2E scenario. | ||
|
|
||
| The Lightspeed Core Stack exports spans/events to the mock OTLP/HTTP collector | ||
| (the ``mock-otel`` Docker Compose service) from startup: the ``OTEL_*`` | ||
| environment variables that enable ``opentelemetry-instrument`` and point the | ||
| exporter at the collector are baked into the Compose files, so no per-scenario | ||
| reconfiguration is needed. These steps only reset the collector's buffer at the | ||
| start of the scenario and assert that telemetry containing a scenario marker is | ||
| delivered. | ||
| """ | ||
|
|
||
| import os | ||
| import time | ||
|
|
||
| import requests | ||
| from behave import given, then # pyright: ignore[reportAttributeAccessIssue] | ||
| from behave.runner import Context | ||
|
|
||
| from tests.e2e.utils.utils import wait_for_container_health | ||
|
|
||
| # Compose service / container name for the mock collector (see docker-compose*.yaml). | ||
| MOCK_OTEL_SERVICE = "mock-otel" | ||
|
|
||
| # Host-side control API of the mock collector (published port from docker-compose). | ||
| _MOCK_OTEL_HOST = os.getenv("E2E_OTEL_MOCK_HOST", "localhost") | ||
| _MOCK_OTEL_PORT = os.getenv("E2E_OTEL_MOCK_PORT", "4318") | ||
| MOCK_OTEL_CONTROL_BASE = f"http://{_MOCK_OTEL_HOST}:{_MOCK_OTEL_PORT}" | ||
|
|
||
| # Delivery is asynchronous: the SDK batches spans before export. Poll generously. | ||
| _DELIVERY_TIMEOUT_S = float(os.getenv("E2E_OTEL_DELIVERY_TIMEOUT_S", "45")) | ||
| _DELIVERY_POLL_INTERVAL_S = 2.0 | ||
|
|
||
|
|
||
| def _reset_mock_collector() -> None: | ||
| """Clear any telemetry buffered by the mock collector from prior runs.""" | ||
| response = requests.post(f"{MOCK_OTEL_CONTROL_BASE}/reset", timeout=5) | ||
| assert ( | ||
| response.status_code == 200 | ||
| ), f"Failed to reset mock OTEL collector: HTTP {response.status_code}" | ||
|
|
||
|
|
||
| def _poll_collector_contains(marker: str) -> bool: | ||
| """Return True once the collector has buffered a payload containing ``marker``.""" | ||
| url = f"{MOCK_OTEL_CONTROL_BASE}/received" | ||
| deadline = time.monotonic() + _DELIVERY_TIMEOUT_S | ||
| while time.monotonic() < deadline: | ||
| try: | ||
| response = requests.get(url, params={"contains": marker}, timeout=5) | ||
| if response.status_code == 200 and response.json().get("found"): | ||
| return True | ||
| except requests.RequestException: | ||
| pass | ||
| time.sleep(_DELIVERY_POLL_INTERVAL_S) | ||
| return False | ||
|
|
||
|
|
||
| @given("An OpenTelemetry service is running and listening for OTLP data") | ||
| def otel_service_running(context: Context) -> None: | ||
| """Wait for the mock OTLP collector to be healthy and clear its buffer. | ||
|
|
||
| The ``mock-otel`` Compose service starts with the rest of the stack and its | ||
| readiness is enforced by the Compose healthcheck, so this step waits for | ||
| that health status and resets any previously buffered telemetry so the | ||
| scenario starts from a clean slate. The Lightspeed Core Stack already | ||
| exports to the collector via the ``OTEL_*`` variables set in the Compose | ||
| files. | ||
| """ | ||
| wait_for_container_health(MOCK_OTEL_SERVICE) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there are two healthchecks for the same thing, the only thing that should be realistically left in this step definition is the reset of mock collector |
||
| _reset_mock_collector() | ||
| context.otel_collector_ready = True | ||
|
|
||
|
|
||
| @then("The OpenTelemetry service received data containing {marker}") | ||
| def collector_received_data(context: Context, marker: str) -> None: | ||
| """Assert the mock collector buffered telemetry containing ``marker``. | ||
|
|
||
| Verifies delivery from the collector's perspective; polls to tolerate the | ||
| SDK's batched, asynchronous export. | ||
| """ | ||
| assert getattr( | ||
| context, "otel_collector_ready", False | ||
| ), "The OpenTelemetry service must be started before asserting on delivery" | ||
| marker = marker.strip() | ||
| assert _poll_collector_contains(marker), ( | ||
| f"Mock OTEL collector did not receive data containing {marker!r} " | ||
| f"within {_DELIVERY_TIMEOUT_S:.0f}s" | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| FROM python:3.12-slim | ||
| WORKDIR /app | ||
| COPY server.py . | ||
| EXPOSE 4318 | ||
| CMD ["python", "server.py"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,53 @@ | ||
| # Mock OTEL collector | ||
|
|
||
| A minimal OTLP/HTTP collector used by the OpenTelemetry E2E scenario | ||
| (`tests/e2e/features/opentelemetry.feature`) to verify that the Lightspeed Core | ||
| Stack delivers spans/events to a telemetry backend. | ||
|
|
||
| It is a stdlib-only `http.server` that buffers the raw OTLP export bodies in | ||
| memory and exposes a small control API so Behave steps can assert what was | ||
| received. See `server.py` for the full endpoint list. | ||
|
|
||
| ## Endpoints | ||
|
|
||
| | Method & path | Purpose | | ||
| | -------------------- | ------------------------------------------------------------- | | ||
| | `POST /v1/*` | Receive an OTLP export (traces/logs/metrics); body buffered. | | ||
| | `GET /received` | Report the count of buffered exports. | | ||
| | `GET /received?contains=<text>` | Report whether `<text>` appears in any payload. | | ||
| | `POST /reset` | Clear the buffer (called at scenario start). | | ||
| | `GET /health` | Liveness probe (`{"status": "ok"}`). | | ||
|
|
||
| Substring queries search the raw request bytes. OTLP protobuf encodes string | ||
| fields as UTF-8, so a plaintext marker embedded in a span attribute value is | ||
| found without decoding protobuf. | ||
|
|
||
| ## Running | ||
|
|
||
| Locally: | ||
|
|
||
| ```bash | ||
| python server.py [port] # default port 4318 | ||
| ``` | ||
|
|
||
| In E2E it runs as the `mock-otel` Docker Compose service on the `lightspeednet` | ||
| network. It starts with the rest of the stack (`docker compose up -d`) and | ||
| `lightspeed-stack` lists it under `depends_on` (waiting for it to become | ||
| healthy), so telemetry is delivered from startup. The | ||
| `An OpenTelemetry service is running and listening for OTLP data` step only waits | ||
| for it to become healthy and resets its buffer. | ||
|
|
||
| ## Pointing the service at it | ||
|
|
||
| The Lightspeed Core Stack exports via HTTP/protobuf when launched with the OTEL | ||
| SDK enabled. The Compose files set these by default so export is always on in | ||
| E2E (override or set `OTEL_SDK_DISABLED=true` to change or disable it): | ||
|
|
||
| ```bash | ||
| OTEL_SDK_DISABLED=false | ||
| OTEL_EXPORTER_OTLP_ENDPOINT=http://mock-otel:4318 | ||
| OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf | ||
| ``` | ||
|
|
||
| The `scripts/entrypoint.sh` gate launches the service under | ||
| `opentelemetry-instrument` whenever `OTEL_SDK_DISABLED=false`. |
Uh oh!
There was an error while loading. Please reload this page.