Skip to main content
New: LLM Observability is now GA

Instrument your application

Zero-code instrumentation for Node.js, Python, Java and .NET, an explicit setup for Go, and the four environment variables all of them read.

Before you start

aiAxonIQ speaks OTLP, so instrumentation is the upstream OpenTelemetry SDK for your language with an endpoint and a header set. There is nothing vendor-specific to install, and nothing to change if you later add a second backend.

Most languages need no code changes at all.

The four variables

Every OpenTelemetry SDK reads the same environment variables, and for most services setting them is the whole integration:

bash
export OTEL_EXPORTER_OTLP_ENDPOINT="https://your-endpoint"   # from Get Started
export OTEL_EXPORTER_OTLP_HEADERS="X-License-Key=oiq_your_key"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_SERVICE_NAME="checkout-api"

Three things about these are worth knowing before you debug anything:

  • OTEL_EXPORTER_OTLP_ENDPOINT is a base URL. The SDK appends /v1/traces, /v1/metrics and /v1/logs itself. Including the signal path produces requests to /v1/traces/v1/traces, which is a 404 that reads like a wrong endpoint.
  • OTEL_SERVICE_NAME is what you will search by. Without it, records still arrive but appear under no service, which is the first place anyone looks.
  • http/protobuf is the default in newer SDKs and not in older ones. Set it explicitly rather than relying on the version you happen to have.

The dashboard's Get Started page renders these already filled in with this workspace's endpoint and a key you create there. If you only want the copy-paste version, use that page rather than this one.

Node.js

bash
npm install @opentelemetry/api @opentelemetry/auto-instrumentations-node
bash
node --require @opentelemetry/auto-instrumentations-node/register your-app.js

The register hook installs HTTP, database and framework instrumentation before your application module loads — which is why it is --require and not an import at the top of your entry file. An import runs after the modules it needs to patch have already been resolved, and produces an app that starts cleanly and emits nothing.

Python

bash
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
bash
opentelemetry-instrument python your_app.py

opentelemetry-bootstrap inspects what you already import and installs instrumentation only for those libraries, so the list stays in step with your dependencies rather than with a snapshot of them.

Java

bash
curl -LO https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
bash
java -javaagent:./opentelemetry-javaagent.jar -jar your-app.jar

The agent instruments the JVM at class-load time. No rebuild, no source change, and it covers most common frameworks and drivers out of the box.

.NET

bash
dotnet add package OpenTelemetry.AutoInstrumentation
bash
./instrument.sh dotnet run

Go

Go has no runtime agent — the compiler resolves calls at build time, so there is nothing to attach to a running process. The exporter is constructed explicitly:

go
package main

import (
	"context"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
)

func InitTracing(ctx context.Context) (func(context.Context) error, error) {
	exporter, err := otlptracehttp.New(ctx,
		// host[:port] plus an optional path — never a scheme. Passing
		// "https://host" here produces a dial error that reads like a
		// network fault rather than a configuration one.
		otlptracehttp.WithEndpoint("your-endpoint-host"),
		otlptracehttp.WithHeaders(map[string]string{
			"X-License-Key": os.Getenv("OIQ_LICENSE_KEY"),
		}),
	)
	if err != nil {
		return nil, err
	}

	provider := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(resource.NewWithAttributes(
			semconv.SchemaURL,
			semconv.ServiceName("checkout-api"),
		)),
	)
	otel.SetTracerProvider(provider)

	return provider.Shutdown, nil
}

Call it from main and defer the returned shutdown. Skipping the shutdown loses whatever is still in the batcher when the process exits, which is reliably the spans from the request you were testing with.

Any other language

If there is no SDK you want to use, the ingest surface is plain OTLP/HTTP with a JSON body. Send data with OpenTelemetry has a complete request you can adapt, and Ingest endpoints and errors documents every path and status code.

Sending logs as well as traces

Auto-instrumentation covers traces and metrics everywhere; log export is per-language and less uniform. Two approaches, both fine:

  • Enable the SDK's log exporter where your language supports it. Records arrive already correlated with the trace that produced them.
  • Write structured logs to stdout and collect them with an OpenTelemetry Collector — the usual choice in Kubernetes, where the collector is already running to gather node telemetry. See Send data from Kubernetes.

Whichever you choose, include trace_id in the log record if the SDK does not add it for you. It is what turns a log line into a starting point for an investigation instead of a sentence.

Next steps