Write your first test
Test a behavior of your own Collector config, from fixture to passing assertion.
This page walks through testing one behavior of a config you already have: an error span must reach your alerting exporter. The same steps apply to any behavior.
Point the test file at your config
Create otel-test.yaml next to your Collector config. All paths in it are relative to the test file.
collector: ./collector.yaml
tests: []Write a fixture
A fixture is the OTLP/JSON body an OTLP/HTTP client would POST to /v1/traces. Write the smallest span
that triggers the behavior. Here, a span with an error status (this is fixtures/traces/error-span.json from
the repository):
{
"resourceSpans": [
{
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "payment-api" } },
{ "key": "deployment.environment.name", "value": { "stringValue": "test" } }
]
},
"scopeSpans": [
{
"scope": { "name": "otel-test.fixtures", "version": "1.0.0" },
"spans": [
{
"traceId": "5b8efff798038103d269b633813fc60e",
"spanId": "eee19b7ec3c1b176",
"name": "POST /api/charge",
"kind": 2,
"startTimeUnixNano": "1735689600000000000",
"endTimeUnixNano": "1735689600120000000",
"attributes": [
{ "key": "http.request.method", "value": { "stringValue": "POST" } },
{ "key": "http.route", "value": { "stringValue": "/api/charge" } },
{ "key": "http.response.status_code", "value": { "intValue": "500" } },
{ "key": "error.type", "value": { "stringValue": "503" } }
],
"status": { "code": 2, "message": "card processor unavailable" }
}
]
}
]
}
]
}Watch for the OTLP/JSON rules: IDs are hex, 64-bit integers are strings, enums are numbers
("code": 2 is STATUS_CODE_ERROR). See Span fixtures.
Add a test with assertions
Use the exporter IDs from your config, exactly as written there.
collector: ./collector.yaml
tests:
- name: errors reach alerting
input: fixtures/error-span.json
expect:
- exporter: otlp_grpc/alerts
count: 1
- attribute: http.response.status_code
exporter: otlp_grpc/alerts
equals: 500equals: 500 is an integer, matching the fixture's intValue. "500" in quotes would be a string and would
fail. See Attribute assertions.
Validate before running
otel-test validatevalidate checks the test file, the config and every fixture, prints how the config is rewritten, runs
otelcol validate on the result, and lists every planned test with its assertions. It doesn't run the tests.
If a name doesn't match your config, you'll see something like:
error: test specification otel-test.yaml does not match the Collector configuration:
- test "errors reach alerting".expect[0]: exporter "alerts" is not used by any pipeline (exporters: otlp_http/acme, otlp_grpc/alerts)Run it
otel-test testIf the assertion fails, the output says what arrived where:
✗ errors reach alerting
✗ exporter otlp_grpc/alerts receives 1 item(s)
Expected: 1 span
Actual: 0 spans
Exporter: otlp_grpc/alerts
Received: otlp_http/acme: 1 spanMake the test meaningful
- Add a negative case. A routing test is stronger with
count: 0on the exporters that must not receive the span. - Guard against over-redaction. Assert that attributes you need still survive (
equals,present), not only that secrets are gone. - Test that the test can fail. Make the change you're guarding against, then run the suite and confirm it fails. The examples ship patches that do exactly this.
If your config has several receivers
When more than one receiver feeds the fixture's signal, the test must say which one to inject through:
defaults:
receiver: otlp
tests:
- name: file logs get the same treatment
input: fixtures/json-body-log.json
receiver: filelog/app
expect:
- attribute: password
absent: trueThe fixture is posted to the replacement for filelog/app, so it enters the same pipelines that receiver feeds.
The filelog receiver itself does not run.