QA How-To
Export k6 Traces to OpenTelemetry: k6 Export Traces OpenTelemetry Tutorial
Follow this k6 export traces opentelemetry tutorial to send HTTP spans through an OTLP Collector, verify trace data, and debug common OTLP setup failures.
18 min read | 2,837 words
TL;DR
Run an OTLP-enabled OpenTelemetry Collector, then execute k6 with `--traces-output=otel=http://127.0.0.1:4318,proto=http`. The Collector debug exporter should print spans for k6 HTTP requests. This trace output is different from `-o opentelemetry`, which sends metrics.
Key Takeaways
- Use k6's built-in --traces-output=otel option to export HTTP request spans over OTLP.
- Keep trace export separate from --out opentelemetry, which exports k6 metrics rather than traces.
- Start with an OpenTelemetry Collector debug exporter so you can prove ingestion before adding a backend.
- Choose OTLP gRPC on port 4317 or OTLP HTTP on port 4318 and make the endpoint and protocol agree.
- Verify both the Collector logs and k6 checks so telemetry success never hides application failure.
- Propagate W3C trace context when you need k6 requests to connect with application-side spans.
- Control trace volume during load tests to protect the generator and observability pipeline.
A k6 export traces opentelemetry tutorial should end with evidence, not merely a command that exits without error. In this guide, you will run a local OpenTelemetry Collector, export k6 HTTP request spans through OTLP, inspect the received data, and then add trace-context propagation for end-to-end analysis.
This focused tutorial belongs to the k6 performance engineering complete guide. Use that pillar when you need workload modeling, thresholds, CI strategy, and result analysis around the tracing workflow covered here.
The key distinction is simple: --traces-output=otel sends traces, while --out opentelemetry sends k6 metrics. You can enable both, but they solve different observability problems. The examples below use the built-in trace output in k6 v2.0.0, so you do not need a custom xk6 binary.\n\n## TL;DR: k6 export traces opentelemetry tutorial\n\nStart an OTLP-enabled Collector, run k6 run --traces-output=otel=http://127.0.0.1:4318,proto=http script.js, and confirm that the Collector debug exporter prints spans. Keep k6 checks and thresholds as the application verdict. Use --out opentelemetry only when you also want k6 metrics because it does not replace trace export.\n\n## What You Will Build
By the end, you will have a small, repeatable observability lab that does four things:
- Runs an OpenTelemetry Collector with OTLP gRPC and HTTP receivers.
- Executes a threshold-backed k6 HTTP test against a configurable target.
- Exports k6 request spans over OTLP and prints them with the Collector debug exporter.
- Adds W3C trace context to requests so an instrumented service can join its spans to the same trace.
- Provides commands for local verification and a safe path toward a production trace backend.
The local debug exporter is intentional. It removes Grafana, Tempo, Jaeger, authentication, and network policy from the first proof. Once spans appear in the Collector logs, replace or supplement the debug exporter with your real backend exporter.
Prerequisites
Use k6 v2.0.0 and OpenTelemetry Collector v0.153.0, the current stable releases used for this 2026 tutorial. Confirm the installed k6 binary and trace option:
k6 version # expect k6 v2.0.0\nk6 run --help | grep traces-output
You also need Docker Engine with Docker Compose v2, a shell, and ports 4317 and 4318 available. Check Docker with:
docker version
docker compose version
Create an empty working directory outside your application repository, then put the three files from this tutorial in it. On Windows, run the commands in Git Bash or translate line continuations for PowerShell. The test target defaults to https://test.k6.io; use a system you own or have explicit permission to test before increasing traffic.
| Component | Purpose | Verification |
|---|---|---|
| k6 | Generates requests and request spans | k6 run --help lists --traces-output |
| OpenTelemetry Collector | Receives, processes, and exports OTLP spans | Container logs show a ready service |
| Debug exporter | Prints received telemetry for the lab | Logs contain ResourceSpans or span fields |
| Instrumented target | Continues the incoming trace context | Backend shows server spans with the same trace ID |
Step 1: Configure the OpenTelemetry Collector
Create otel-collector-config.yaml:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch: {}
exporters:
debug:
verbosity: detailed
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [debug]
The OTLP receiver accepts both standard transports. Port 4317 is for OTLP over gRPC. Port 4318 is for OTLP over HTTP/protobuf. The batch processor groups spans before export, which better resembles a real pipeline. The debug exporter writes decoded telemetry to the Collector logs.
Do not add a metrics pipeline yet. This lab proves traces. A metrics pipeline is useful later, but it can blur the distinction between k6 result metrics and request spans.
Verify the step: validate that the file exists and that indentation uses spaces. The Collector performs the definitive configuration validation at startup in Step 2. If the YAML is malformed or a component name is unavailable, the container exits with a specific configuration error.
Step 2: Run the Collector in Docker
Create compose.yaml beside the configuration file:
services:
otel-collector:
image: otel/opentelemetry-collector:0.153.0
command: ["--config=/etc/otelcol/config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otelcol/config.yaml:ro
ports:
- "4317:4317"
- "4318:4318"
The pinned image makes the lab repeatable. Update it deliberately after checking the Collector release notes and validating the configuration in a branch. Production deployments should also set resource limits, health checks, authentication, and TLS according to the environment.
Start the service:
docker compose up -d
docker compose ps
docker compose logs otel-collector
The published ports make the Collector reachable from k6 running on the host. If k6 also runs in Compose, use the service name and container port instead, such as http://otel-collector:4318. Inside a container, 127.0.0.1 refers to that container, not your host or the Collector container.
Verify the step: docker compose ps should show the Collector as running. Its logs should show that the OTLP receiver started and the service is ready. No spans appear yet because no producer has sent them. An immediate exit usually means invalid YAML, an unavailable port, or a volume path problem.
Step 3: Write a Threshold-Backed k6 Test
Create script.js:
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
vus: 2,
duration: '10s',
discardResponseBodies: true,
thresholds: {
http_req_failed: ['rate<0.01'],
http_req_duration: ['p(95)<1000'],
},
};
const baseUrl = __ENV.BASE_URL || 'https://test.k6.io';
export default function () {
const response = http.get(`${baseUrl}/`, {
tags: { operation: 'home-page' },
});
check(response, {
'home page returns 200': (res) => res.status === 200,
});
sleep(1);
}
This is deliberately small. Two virtual users run for ten seconds, request one endpoint, assert the status, and enforce failure-rate and latency thresholds. discardResponseBodies reduces generator work because the check needs only the status. The custom operation tag helps organize k6 metrics, although trace attributes depend on what the trace output emits.
Keep load low while validating telemetry. Trace pipelines can amplify data volume, and debugging thousands of spans is harder than debugging twenty. After the lab works, restore your realistic scenarios, arrival rates, and tags.
Verify the step: run the script once without trace export:
k6 run script.js
The summary should report successful checks and passed thresholds. Fix DNS, TLS, target permissions, and response errors now. A clean baseline separates test-script failures from OTLP export failures.
Step 4: Export k6 Traces to OpenTelemetry over HTTP
Run the same test with the built-in trace output:
k6 run \
--traces-output=otel=http://127.0.0.1:4318,proto=http \
script.js
The option has two important parts. otel= selects the OpenTelemetry-compatible trace exporter and supplies its endpoint. proto=http selects OTLP over HTTP. The Collector's HTTP receiver accepts the standard trace request below /v1/traces; k6 handles the protocol request construction. Do not append /v1/traces to this k6 endpoint syntax unless the k6 option documentation for your installed version explicitly requires it.
In another terminal, follow the Collector logs:
docker compose logs -f otel-collector
You should see trace batches printed by the debug exporter. Detailed field labels can vary by Collector release, but expect resource spans, scope spans, trace IDs, span IDs, timestamps, and HTTP-related attributes. The k6 process can still complete when a remote exporter loses some data, so Collector-side evidence matters.
Verify the step: satisfy both checks. First, k6 must pass its application checks and thresholds. Second, Collector logs must contain decoded spans created during the run. Record the run time and one trace ID so you can find the same event after connecting a backend.
Step 5: Compare OTLP HTTP and gRPC
The default OTLP trace transport is gRPC. Test it against port 4317:
k6 run \
--traces-output=otel=http://127.0.0.1:4317,proto=grpc \
script.js
Use an endpoint and protocol pair that agree. Sending HTTP/protobuf to 4317, or gRPC to 4318, produces connection, framing, or unimplemented-service errors. A proxy may also support ordinary HTTP while failing gRPC, which makes HTTP/protobuf a practical choice in some CI networks.
| Choice | Typical endpoint | Strength | Common issue |
|---|---|---|---|
| OTLP gRPC | http://host:4317 |
Efficient streaming RPC and common native default | HTTP-only proxies can block or mishandle it |
| OTLP HTTP/protobuf | http://host:4318 |
Often simpler through proxies and ingress | Wrong path or port causes 404 or protocol errors |
| Direct backend export | Vendor-specific | Fewer local components | Couples tests to credentials and backend availability |
| Collector gateway | Local or shared OTLP endpoint | Central retry, processing, routing, and redaction | Requires operating the Collector pipeline |
Prefer the Collector gateway pattern for shared environments. It keeps backend credentials out of test scripts and gives you one place to apply batching, filtering, sampling, memory limits, and retries.
Verify the step: clear or timestamp the log view, run the gRPC command, and confirm a new trace batch appears. If HTTP works but gRPC does not, the k6 script is fine. Inspect the port mapping, proxy path, firewall, and receiver protocol.
Step 6: Add W3C Trace Context Propagation
Exporting k6 spans and propagating context are related but separate jobs. Export creates client-side telemetry. Propagation puts a trace identifier into the outgoing request so an instrumented target can create server spans in the same distributed trace.
For a target that accepts W3C Trace Context, generate a valid traceparent value per request. Current k6 includes tracing support, but API stability and packaging can differ across releases. A portable option is the official k6 HTTP instrumentation library. Create propagated-script.js:
import { check, sleep } from 'k6';
import tempo from 'https://jslib.k6.io/http-instrumentation-tempo/1.0.1/index.js';
const http = new tempo.Client({
propagator: 'w3c',
});
export const options = {
vus: 1,
iterations: 5,
thresholds: {
http_req_failed: ['rate<0.01'],
},
};
const baseUrl = __ENV.BASE_URL || 'https://test.k6.io';
export default function () {
const response = http.get(`${baseUrl}/`);
check(response, {
'instrumented request returns 200': (res) => res.status === 200,
});
sleep(1);
}
The client is a drop-in wrapper around k6 HTTP calls and attaches W3C trace context. For controlled CI, vendor the reviewed library file or bundle dependencies instead of downloading code during every test. Your application must be instrumented to extract traceparent; otherwise it will treat the header as ordinary metadata and create no connected server span.
Run it with trace export:
k6 run \
--traces-output=otel=http://127.0.0.1:4318,proto=http \
propagated-script.js
Verify the step: first confirm k6 and Collector success as before. Then query your application trace backend and check that a client span and server span share the same trace ID with a parent-child relationship. The public default target is not your verification backend, so use an instrumented BASE_URL for this end-to-end check.
Step 7: Send Traces to a Real Backend Safely
Replace the debug-only destination by adding an exporter supported by your Collector distribution. The exact exporter block depends on Tempo, Jaeger, Grafana Cloud, or another OTLP backend, so take the endpoint and authentication format from that backend's official documentation. Keep debug temporarily while onboarding:
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [debug, otlphttp/your_backend]
Define otlphttp/your_backend under exporters with environment-variable substitution for secrets. Never commit API keys in Collector YAML, Compose files, or k6 scripts. Use the CI secret store, container secret mechanism, or workload identity. Enable TLS verification outside an isolated local lab.
Add resource attributes that identify environment, test run, service, and build where your selected k6 and Collector versions support them. Avoid high-cardinality labels such as random user IDs in metrics. Trace IDs are inherently unique, but arbitrary attributes still increase storage and query costs. Apply sampling with intent: a tiny smoke run may keep all spans, while a sustained load run usually needs a controlled policy.
Verify the step: keep the debug exporter active and compare one known trace ID in both Collector logs and the backend. Then temporarily use an invalid credential in a nonproduction environment and confirm the Collector reports export failure clearly. Restore the secret and ensure queued data drains without continuous retry errors.
Step 8: Run the Workflow in CI
In CI, start the Collector before k6, wait for readiness, run the test, and always capture Collector logs. A generic shell sequence looks like this:
set -eu
docker compose up -d
trap 'docker compose logs otel-collector; docker compose down' EXIT
for attempt in 1 2 3 4 5; do
if docker compose ps --status running | grep -q otel-collector; then
break
fi
sleep 2
done
k6 run \
--traces-output=otel=http://127.0.0.1:4318,proto=http \
script.js
A running container is a basic gate, not a full OTLP health probe. For a hardened pipeline, enable the Collector health-check extension or send a known canary trace and assert it reaches the expected destination. Preserve logs as CI artifacts, but review them for sensitive URL parameters and headers first.
Treat performance assertions and telemetry assertions separately. The k6 exit code should fail the job when thresholds fail. A missing-traces check should fail or warn according to your observability requirement. This prevents a healthy target from masking a broken trace pipeline and prevents successful trace export from masking a slow target.
Verify the step: inspect the CI job output for passed k6 thresholds, a clean Collector startup, at least one received trace batch, and no persistent export errors. Run a deliberate negative test by changing the endpoint port, then confirm your pipeline surfaces the telemetry failure rather than silently publishing a green result.
Troubleshooting
Problem: the k6 binary reports unknown flag --traces-output -> Your installed binary predates or omits the current trace-output capability. Check k6 version and the official option reference, then install a current official release. Do not substitute -o opentelemetry; that output sends metrics, not request traces.
Problem: connection refused on 127.0.0.1:4318 -> Confirm the Collector container is running and the port is published. If k6 runs in another container, address otel-collector:4318 on the Compose network instead of localhost. Check docker compose ps and Collector startup logs.
Problem: Collector returns 404, unimplemented, or protocol errors -> Match proto=http with the HTTP receiver on 4318, or proto=grpc with the gRPC receiver on 4317. Remove reverse proxies until direct export works, then configure explicit gRPC or OTLP HTTP routing.
Problem: k6 passes but no spans appear -> Follow Collector logs during the run, confirm the traces pipeline includes the otlp receiver and debug exporter, and verify the exact endpoint passed to k6. A metrics-only pipeline cannot process traces. Also distinguish the trace flag from the metrics output flag.
Problem: client and server spans have different trace IDs -> Export alone does not guarantee propagation. Use the W3C instrumentation client, ensure the application extracts traceparent, and check that a gateway is not stripping the header. Confirm the service's OpenTelemetry SDK uses a compatible propagator.
Problem: the generator or Collector consumes too many resources -> Reduce VUs and duration while debugging, batch spans, apply intentional sampling, and set Collector memory controls. Do not turn on detailed debug export during a high-volume production-like test because log rendering itself adds substantial work.
Common Mistakes and Best Practices
- Do start with one VU and a debug exporter. Scale only after you can locate a known trace.
- Do keep k6 checks and thresholds enabled. Telemetry is diagnostic evidence, not the pass condition for application behavior.
- Do pin Collector images and remote JavaScript dependencies in team repositories. Review upgrades through the normal dependency process.
- Do use a Collector to centralize credentials, retries, filtering, and routing.
- Do tag runs with stable identifiers that map to a build, environment, and test name.
- Do not confuse
--out opentelemetrymetrics with--traces-output=oteltraces. - Do not send realistic load to a public demo endpoint. Use an authorized target and documented test window.
- Do not disable TLS verification in shared or production environments. Install the correct certificate authority instead.
- Do not log sensitive query strings, headers, or customer data through span attributes. Redact at instrumentation and Collector layers.
- Do not keep every span from a large endurance test without estimating pipeline and backend capacity.
Where To Go Next
Return to the complete k6 performance engineering guide to place tracing inside a full test strategy. If the workload depends on user experience, continue with k6 browser Web Vitals testing. For larger execution capacity, use the k6 Kubernetes Operator distributed load testing tutorial. If your system streams long-lived responses, follow the k6 Server-Sent Events testing guide.
A sensible progression is local proof, instrumented staging target, backend export, CI canary, and finally controlled load. At every stage, save one trace ID alongside the k6 run identifier. That small habit makes test results and service telemetry much easier to correlate during an incident review.
k6 Export Traces OpenTelemetry Tutorial Interview Questions and Answers
Q: What is the difference between k6 OpenTelemetry metrics output and traces output?
--out opentelemetry maps k6 result metrics such as counters, rates, gauges, and trends into OpenTelemetry metrics. --traces-output=otel sends trace spans for request activity. They can share a Collector, but they require separate signal pipelines and answer different questions.
Q: Why place an OpenTelemetry Collector between k6 and the backend?
The Collector decouples the test from a vendor destination. It can batch, retry, filter, redact, sample, authenticate, and route spans without embedding those responsibilities in the script. It also provides a useful debug boundary when export fails.
Q: How do you choose between OTLP gRPC and OTLP HTTP?
Use the transport supported end to end by the network and backend. gRPC commonly uses port 4317; HTTP/protobuf commonly uses 4318. HTTP can be easier through conventional proxies, while gRPC is a common native OTLP default.
Q: Does exporting k6 traces automatically create end-to-end distributed traces?
No. The request must carry compatible trace context, and the target service must extract it and create server-side spans. Export and propagation are separate capabilities that you verify independently.
Q: How do you prove the setup works?
Pass the k6 checks and thresholds, observe decoded spans at the Collector, and locate a known trace ID in the final backend. For end-to-end tracing, also verify that client and server spans share a trace ID and have the expected parent relationship.
Q: What changes for high-volume load tests?
Disable detailed debug logging, batch spans, size the Collector, and apply an intentional sampling policy. Monitor the load generator so telemetry overhead does not distort the workload. Keep stable run identifiers while avoiding sensitive or unbounded attributes.
Conclusion
The reliable way to complete a k6 export traces OpenTelemetry tutorial is to verify each boundary: k6 request behavior, OTLP transport, Collector ingestion, backend export, and trace-context continuation. The working core is --traces-output=otel pointed at an OTLP receiver whose protocol and port match.
Keep the first test small, prove it with the Collector debug exporter, and then add your real backend, security controls, and sampling. Once one known trace connects the load generator to an instrumented service, you have a strong foundation for explaining latency instead of merely observing it.
Interview Questions and Answers
How would you export k6 request traces through OpenTelemetry?
I would run an OTLP receiver in an OpenTelemetry Collector and execute k6 with `--traces-output=otel` using the matching endpoint and protocol. I would first enable the Collector debug exporter, keep the workload small, and verify decoded spans. Then I would add the production backend exporter and locate the same trace ID there.
What is the difference between trace export and trace propagation in k6?
Trace export sends k6-created span data to an OTLP destination. Trace propagation adds context such as a W3C `traceparent` header to the request so a service can continue the trace. End-to-end distributed tracing needs both export and compatible server-side extraction.
Why use an OpenTelemetry Collector instead of exporting directly to a trace backend?
A Collector separates the test from vendor credentials and endpoint details. It can batch, retry, sample, redact, enrich, and route telemetry. It also creates a clear diagnostic boundary between generation and backend ingestion.
How would you troubleshoot an OTLP protocol error?
I would verify the full transport tuple: k6 protocol, destination port, Collector receiver, proxy support, and TLS mode. I would test HTTP/protobuf directly on 4318 or gRPC directly on 4317 without a proxy. If one works, I would add network layers back one at a time.
How do you prevent tracing from distorting a performance test?
I measure generator CPU, memory, and network use with tracing enabled and disabled. I avoid detailed debug export at scale, use batching and intentional sampling, and size the Collector separately. I also keep the test assertions unchanged so telemetry overhead cannot redefine success.
What evidence proves an end-to-end k6 tracing setup is correct?
The k6 checks and thresholds pass, the Collector receives spans without persistent export errors, and the backend contains a known trace ID from the run. For a distributed trace, the client and server spans share that trace ID and show the expected parent-child chain.
How would you secure k6 trace export in CI?
I would send telemetry to a controlled Collector over verified TLS and keep backend credentials in the CI secret store or workload identity. I would redact sensitive attributes, restrict network access, pin dependencies, and retain sanitized diagnostic logs. I would never hardcode tokens in the k6 script or committed Collector configuration.
Frequently Asked Questions
Can k6 export traces directly to OpenTelemetry?
Yes. A current k6 release can send request traces to an OpenTelemetry-compatible endpoint with `--traces-output=otel`. Point it at an OTLP Collector or compatible backend and select a matching gRPC or HTTP protocol.
Is k6 --out opentelemetry the same as traces output?
No. `--out opentelemetry` exports k6 metrics, while `--traces-output=otel` exports traces. A Collector needs the corresponding metrics or traces pipeline for each signal you enable.
Which ports does k6 use for OTLP traces?
OTLP gRPC conventionally uses port 4317, and OTLP HTTP/protobuf conventionally uses port 4318. Your k6 protocol option, Collector receiver, port mapping, proxy, and backend must agree.
Why are no k6 spans visible in the Collector?
Confirm the Collector has an OTLP receiver in a traces pipeline and that k6 points to a reachable endpoint. Check that you used `--traces-output`, not only the metrics output, and inspect logs for protocol or port mismatches.
Do I need xk6 to export OpenTelemetry traces?
Not for the built-in trace output in a current official k6 release. Verify your binary exposes `--traces-output`; upgrade it if the flag is unknown. Extensions may still serve specialized integrations, but they are not required for this workflow.
How do I correlate k6 requests with application traces?
Send W3C Trace Context with the request and instrument the target service to extract it. Then verify the k6 client span and application server span use the same trace ID and correct parent relationship.
Should I export every span during a large k6 test?
Usually you should make that a deliberate capacity decision. Use batching, controlled sampling, Collector resource limits, and backend quotas so trace processing does not distort the load generator or overload the observability system.