OpenTelemetry Acceptance Tests (OATs)
OpenTelemetry Acceptance Tests (OATs), or OATs for short, is a test framework for OpenTelemetry.
- Declarative tests written in YAML
- Supported signals: traces, logs, metrics
- Full round-trip testing: from the application to the observability stack
- Data is stored in the LGTM stack (Loki, Grafana, Tempo, Prometheus, OpenTelemetry Collector)
- Data is queried using LogQL, PromQL, and TraceQL
- All data is sent to the observability stack via OTLP - so OATs can also be used with other observability stacks
- End-to-end testing
Installation
- Install the
oats binary:
go install github.com/grafana/oats@latest
- You can confirm it was installed with:
❯ ls $GOPATH/bin
oats
Getting Started
[!TIP]
You can use the test cases in prom_client_java as a reference.
The GitHub action
uses mise run acceptance-test to run the tests.
-
Create a folder oats-tests for the following files
-
Create Dockerfile to build the application you want to test
FROM eclipse-temurin:21-jre
COPY target/example-exporter-opentelemetry.jar ./app.jar
ENTRYPOINT [ "java", "-jar", "./app.jar" ]
-
Create docker-compose.yaml to start the application and any dependencies
services:
java:
build:
dockerfile: Dockerfile
environment:
OTEL_SERVICE_NAME: "rolldice"
OTEL_EXPORTER_OTLP_ENDPOINT: http://lgtm:4318
OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
-
Create oats.yaml with the test cases
# OATs is an acceptance testing framework for OpenTelemetry - https://github.com/grafana/oats
oats-schema-version: 2
docker-compose:
files:
- ./docker-compose.yaml
expected:
metrics:
- promql: "uptime_seconds_total{}"
value: ">= 0"
-
Run the tests:
oats /path/to/oats-tests/oats.yaml
Running OATs Directly
OATs can be run directly using the command-line interface:
# Run specific test files
oats /path/to/oats-tests/oats.yaml
# Run multiple specific test files
oats /path/to/repo/test1.yaml /path/to/repo/test2.yaml
# Run all tests in a directory (scans for .yaml/.yml files with oats-schema-version)
oats /path/to/oats-tests
# With flags
oats --timeout=1m --lgtm-version=latest --manual-debug=false /path/to/oats-tests/oats.yaml
Running multiple tests
It can run multiple tests:
# Scan directory for all test files
oats /path/to/repo
# Or specify individual test files (better performance)
oats /path/to/repo/test1.yaml /path/to/repo/test2.yaml
When scanning a directory, OATs will search for all .yaml and .yml files
that contain the oats-schema-version tag. Files marked with
oats-template: true will be skipped as entry points but can still be included
by other test files.
Flags
The following flags are available:
-timeout: Set the timeout for test cases (default: 30s)
-absent-timeout: Set the timeout for tests that assert absence (default: 10s)
-lgtm-version: Specify the version of docker-otel-lgtm to use (default: "latest")
-manual-debug: Enable debug mode to keep containers running (default: false)
-lgtm-log-all: Enable logging for all containers (default: false)
-lgtm-log-grafana: Enable logging for Grafana (default: false)
-lgtm-log-loki: Enable logging for Loki (default: false)
-lgtm-log-tempo: Enable logging for Tempo (default: false)
-lgtm-log-prometheus: Enable logging for Prometheus (default: false)
-lgtm-log-pyroscope: Enable logging for Pyroscope (default: false)
-lgtm-log-collector: Enable logging for OpenTelemetry Collector (default: false)
-host: Override the host used to issue requests to applications and LGTM (default: localhost)
-log-limit: Maximum log output length per log entry
Run OATs in GitHub Actions
The docker-otel-lgtm repo uses
mise to install and run OATs from GitHub Actions. You
can also install OATs directly.
Test Case Syntax
[!TIP]
All test files must include oats-schema-version: 2 at the top level.
Template files (used in include sections) must also include
oats-template: true to prevent them from being run as entry points.
You can use any file name with .yaml or .yml extension.
The syntax is a bit similar to Tracetest.
Here is an example:
oats-schema-version: 2
include:
- ../oats-template.yaml
docker-compose:
files:
- ../docker-compose.yaml
input:
- path: /stock
status: 200 # expected status code, 200 is the default
interval: 500ms # interval between requests to the input URL
expected:
traces:
- traceql: '{ name =~ "SELECT .*product"}'
regexp: "SELECT .*"
attributes:
db.system: h2
logs:
- logql: '{exporter = "OTLP"}'
equals: "hello LGTM"
metrics:
- promql: 'db_client_connections_max{pool_name="HikariPool-1"}'
value: "== 10"
Template Files
Template files are used to share common configuration across multiple test
files. They must include both oats-schema-version and oats-template: true:
# oats-template.yaml
oats-schema-version: 2
oats-template: true
docker-compose:
files:
- ./docker-compose.yaml
Here is another example with a more specific input:
oats-schema-version: 2
include:
- ../oats-template.yaml
docker-compose:
files:
- ../docker-compose.yaml
input:
- path: /users
method: POST
scheme: https
host: 127.0.0.1
status: 201
headers:
Authorization: Bearer my-access-token
Content-Type: application/json
body: |-
{
"name": "Grot"
}
interval: 500ms
expected:
traces:
- traceql: '{ name =~ "SELECT .*product"}'
regexp: "SELECT .*"
attributes:
db.system: h2
Query traces
Each entry in the traces array is a test case for traces.
expected:
traces:
- traceql: '{ name =~ "SELECT .*product"}'
regexp: "SELECT .*"
attributes:
db.system: h2
count:
min: 1 # allow multiple spans with the same attributes
- traceql: '{ span.kind = "client" }'
equals: "HTTP GET"
- traceql: '{ name =~ "dropped-span" }'
count:
max: 0 # assert this span does NOT exist (e.g., filtered/dropped spans)
Trace assertion options
traceql: TraceQL query to find the trace (required)
equals: Exact string match for the span name (any span in the trace)
regexp: Regular expression pattern to match against the span name (any span in the trace)
attributes: Key-value pairs that must match exactly on the span (the span name matched by equals or regexp)
attribute-regexp: Key-value pairs where values are regex patterns to
match against span attributes (the span name matched by equals or
regexp)
no-extra-attributes: Set to true to fail if the span has attributes
beyond those specified in attributes and attribute-regexp
count: Control expected number of matching spans, ignoring if they match other criteria
min: Minimum number of spans expected (default: 1 if not specified)
max: Maximum number of spans expected (0 means no upper limit, or exactly 0 when min is also 0)
- Examples:
- Not specified: at least 1 span expected
{ min: 2, max: 5 }: between 2 and 5 spans (inclusive)
{ min: 3 }: 3 or more spans
{ max: 0 }: exactly 0 spans (assert absence)
matrix-condition: Regex to match against matrix test case names (only run this assertion for matching matrix cases)
Query logs
Each entry in the logs array is a test case for logs.
expected:
logs:
- logql: '{service_name="rolldice"} |~ `Anonymous player is rolling the dice.*`'
equals: "Anonymous player is rolling the dice"
attributes:
service_name: rolldice
attribute-regexp:
container_id: ".*"
no-extra-attributes: true # fail if there are extra attributes
- logql: '{service_name="rolldice"} |~ `Anonymous player is rolling the dice.*`'
regexp: "Anonymous player is .*"
Log assertion options
logql: LogQL query to find the log line (required)
equals: Exact string match for the log line
regexp: Regular expression pattern to match against the log line
attributes: Key-value pairs that must match exactly on the log labels
attribute-regexp: Key-value pairs where values are regex patterns to match against log labels
no-extra-attributes: Set to true to fail if the log has labels beyond those specified in attributes and attribute-regexp
count: Expected count range for returned log lines, ignoring if they match other criteria
min: Minimum expected count (defaults to 0 if not specified)
max: Maximum expected count. Set to 0 for no upper limit. To assert absence, set both min: 0 and max: 0
matrix-condition: Regex to match against matrix test case names
Example:
expected:
logs:
- logql: '{service_name="rolldice"}'
equals: "Rolling dice"
count:
min: 1
max: 5 # expect between 1-5 matching logs (inclusive)
Query metrics
expected:
metrics:
- promql: 'db_client_connections_max{pool_name="HikariPool-1"}'
value: "== 10"
Metric assertion options
promql: PromQL query to retrieve the metric (required)
value: Expected value with comparison operator. Supported operators:
==, !=, >, <, >=, <= (e.g., ">= 0", "== 10")
matrix-condition: Regex to match against matrix test case names
Query profiles
expected:
profiles:
- query: 'process_cpu:cpu:nanoseconds:cpu:nanoseconds{service_name="my-service"}'
flamebearers:
equals: "main"
Profile assertion options
query: Pyroscope query to retrieve the profile (required)
flamebearers: Assertions on the flamebearer response
equals: String that must appear in the flamebearer names
regexp: Regular expression pattern to match against the flamebearer names
matrix-condition: Regex to match against matrix test case names
Custom checks
Custom checks allow you to run arbitrary scripts for advanced validation scenarios.
expected:
custom-checks:
- script: |
#!/bin/bash
# Your custom validation script here
exit 0
Custom check options
script: Script to execute (required)
matrix-condition: Regex to match against matrix test case names
Matrix of test cases
Matrix tests are useful to test different configurations of the same application,
e.g. with different settings of the otel collector or different flags in the application.
matrix:
- name: default
docker-compose:
files:
- ./docker-compose.oats.yml
- name: self-contained
docker-compose:
files:
- ./docker-compose.self-contained.oats.yml
- name: net8
docker-compose:
files:
- ./docker-compose.net8.oats.yml
You can then make test cases depend on the matrix name:
expected:
metrics:
- promql: 'db_client_connections_max{pool_name="HikariPool-1"}'
value: "== 10"
matrix-condition: default
matrix-condition is a regex that is applied to the matrix name. This field is
available for all assertion types (traces, logs, metrics, profiles).
Docker Compose
Describes the docker-compose file(s) to use for the test.
The files typically define the instrumented application you want to test and optionally some dependencies,
e.g. a database server to send requests to.
You don't need (and shouldn't have) to define the observability stack (e.g. Prometheus, Grafana, etc.),
because this is provided by the test framework (and may test different versions of the observability stack,
e.g. OTel Collector and Grafana Alloy).
This docker-compose file is relative to the oats.yaml file.
Kubernetes
A local Kubernetes cluster can be used to test the application in a Kubernetes environment rather than in docker-compose.
This is useful to test the application in a more realistic environment - and when you want to test Kubernetes specific features.
Describes the Kubernetes manifest(s) to use for the test.
kubernetes:
dir: k8s
app-service: dice
app-docker-file: Dockerfile
app-docker-context: ..
app-docker-tag: dice:1.1-SNAPSHOT
app-docker-port: 8080