> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aiaxoniq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 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.

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.

<Info>
  **Before you start.** You need [a license key](/get-started/license-keys) and
  your base endpoint from **Get Started** in the dashboard.
</Info>

## The four variables

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

```bash theme={null}
export OTEL_EXPORTER_OTLP_ENDPOINT="$OIQ_ENDPOINT"          # from Get Started
export OTEL_EXPORTER_OTLP_HEADERS="X-License-Key=$OIQ_LICENSE_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.

<Warning>
  **`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 — and sends people to change the one part that was correct.
</Warning>

* **`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.

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

## Zero-code languages

<Tabs>
  <Tab title="Node.js">
    ```bash theme={null}
    npm install @opentelemetry/api @opentelemetry/auto-instrumentations-node
    ```

    ```bash theme={null}
    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.
  </Tab>

  <Tab title="Python">
    ```bash theme={null}
    pip install opentelemetry-distro opentelemetry-exporter-otlp
    opentelemetry-bootstrap -a install
    ```

    ```bash theme={null}
    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.
  </Tab>

  <Tab title="Java">
    ```bash theme={null}
    curl -LO https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
    ```

    ```bash theme={null}
    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.
  </Tab>

  <Tab title=".NET">
    ```bash theme={null}
    dotnet add package OpenTelemetry.AutoInstrumentation
    ```

    ```bash theme={null}
    ./instrument.sh dotnet run
    ```
  </Tab>
</Tabs>

## 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 theme={null}
package main

import (
	"context"
	"os"

	"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](/send-data/otel/collector) has a
complete request you can adapt, and
[Endpoints and errors](/send-data/endpoints) 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](/send-data/platforms/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.

## Verify

Turn on the SDK's own diagnostics and restart the service:

```bash theme={null}
export OTEL_LOG_LEVEL=debug
```

**Expected output** — the exporter naming your endpoint at startup:

```text theme={null}
@opentelemetry/api: Registered a global for diag@1.x.x
OTLPTraceExporter: sending 12 spans to https://app.aiaxoniq.com/otlp/v1/traces
```

Then exercise the service and confirm it appears: open **Services** in the
dashboard, set the range to the last 15 minutes, and look for the value you set
in `OTEL_SERVICE_NAME`.

## Troubleshooting

<AccordionGroup>
  <Accordion title="The app starts cleanly and emits nothing" icon="ear-listen">
    The instrumentation never loaded. In Node.js this is almost always an
    `import` at the top of the entry file instead of `--require` — by the time
    an import runs, the modules the hook needs to patch are already resolved.

    In Python, confirm you are launching through `opentelemetry-instrument` and
    not running the interpreter directly.
  </Accordion>

  <Accordion title="404 on every export" icon="link-slash">
    `OTEL_EXPORTER_OTLP_ENDPOINT` is a **base** URL. The SDK appends
    `/v1/traces` itself, so a value already ending in `/v1/traces` produces
    requests to `/v1/traces/v1/traces`.
  </Accordion>

  <Accordion title="Spans from my test request are missing" icon="hourglass-end">
    The process exited before the batcher flushed. Short-lived jobs, tests and
    CLI tools need an explicit shutdown — in Go, the `provider.Shutdown` this
    page returns; in Node, `sdk.shutdown()`.

    It is reliably the last batch that is lost, which is the one you were
    watching for.
  </Accordion>

  <Accordion title="Everything arrives under no service name" icon="tag">
    `OTEL_SERVICE_NAME` is unset. Records still arrive and are still queryable,
    but no service-level view can group them, which is where most people look
    first.
  </Accordion>
</AccordionGroup>

## Next

<CardGroup cols={2}>
  <Card title="Verify your data arrived" icon="circle-check" href="/get-started/verify-data">
    Confirm it landed.
  </Card>

  <Card title="Run a collector in Kubernetes" icon="dharmachakra" href="/send-data/platforms/kubernetes">
    Node and cluster telemetry.
  </Card>
</CardGroup>
