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

# Send data from a Linux VM

> Install the OpenTelemetry Collector as a systemd service on Debian, Ubuntu or RHEL, collect host metrics and journald logs, and keep the key out of the config file.

On a virtual machine or a bare-metal host, the collector runs as a native
systemd service. That gives you host metrics, system logs and an OTLP endpoint
for anything running on the box — with restart-on-failure and start-on-boot
handled by the init system rather than by you.

<Info>
  **Before you start, you need two values.**

  1. **Your base endpoint** — shown on **Get Started** in the dashboard. These
     pages write it as `$OIQ_ENDPOINT`.
  2. **A license key** — created in **Settings → License Keys**, starting
     `oiq_`. Requires the Admin role. See
     [Create a license key](/get-started/license-keys). These pages write it as
     `$OIQ_LICENSE_KEY`.

  Export both before running anything below:

  ```bash theme={null}
  export OIQ_ENDPOINT="https://app.aiaxoniq.com/otlp"   # or your own
  export OIQ_LICENSE_KEY="oiq_..."
  ```
</Info>

You also need `sudo` on a host with systemd, and outbound HTTPS to your ingest
endpoint.

## Why a package rather than a container

On a host whose job *is* running your application, a containerised collector
adds a layer that gets in the way of the thing you most want: host metrics. A
container sees its own filesystem and its own process namespace, so collecting
real host telemetry from inside one means mounting the host back in and telling
the receiver where you put it.

Installed natively, it just sees the host. It also restarts with the machine
without a container runtime having to be up first.

## 1. Install the collector

<CodeGroup>
  ```bash Debian / Ubuntu theme={null}
  OTEL_VERSION=0.115.1
  curl -fsSLO "https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${OTEL_VERSION}/otelcol-contrib_${OTEL_VERSION}_linux_amd64.deb"
  sudo dpkg -i "otelcol-contrib_${OTEL_VERSION}_linux_amd64.deb"
  ```

  ```bash RHEL / Rocky / Amazon Linux theme={null}
  OTEL_VERSION=0.115.1
  curl -fsSLO "https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${OTEL_VERSION}/otelcol-contrib_${OTEL_VERSION}_linux_amd64.rpm"
  sudo rpm -ivh "otelcol-contrib_${OTEL_VERSION}_linux_amd64.rpm"
  ```

  ```bash arm64 theme={null}
  # Same URLs with linux_arm64 in place of linux_amd64.
  OTEL_VERSION=0.115.1
  curl -fsSLO "https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v${OTEL_VERSION}/otelcol-contrib_${OTEL_VERSION}_linux_arm64.deb"
  sudo dpkg -i "otelcol-contrib_${OTEL_VERSION}_linux_arm64.deb"
  ```
</CodeGroup>

The package installs the binary, a systemd unit called `otelcol-contrib`, and a
default config at `/etc/otelcol-contrib/config.yaml`. It starts the service
immediately on the default config, which exports nowhere — the next two steps
replace it.

<Tip>
  Pin `OTEL_VERSION` rather than tracking the latest release. The collector's
  configuration schema changes between versions, and an unattended upgrade that
  invalidates your config will stop telemetry at the least convenient moment.
</Tip>

## 2. Supply the endpoint and key

The key must not live in the config file, which is world-readable. systemd reads
an environment file you can lock down instead:

```bash theme={null}
sudo install -m 600 /dev/null /etc/otelcol-contrib/aiaxoniq.env

sudo tee /etc/otelcol-contrib/aiaxoniq.env >/dev/null <<EOF
OIQ_ENDPOINT=${OIQ_ENDPOINT}
OIQ_LICENSE_KEY=${OIQ_LICENSE_KEY}
EOF
```

<Warning>
  **Use `tee` with a heredoc, not `echo` into a file you then edit by hand.** A
  trailing newline or a stray space becomes part of the key value, and the
  result is a `401` that looks nothing like a formatting problem. Mode `600`
  matters too: the default config directory is readable by every user on the
  box.
</Warning>

Point the unit at that file:

```bash theme={null}
sudo systemctl edit otelcol-contrib
```

Add:

```ini theme={null}
[Service]
EnvironmentFile=/etc/otelcol-contrib/aiaxoniq.env
```

## 3. Write the config

```bash theme={null}
sudo tee /etc/otelcol-contrib/config.yaml >/dev/null <<'EOF'
receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 127.0.0.1:4317
      http:
        endpoint: 127.0.0.1:4318
  hostmetrics:
    collection_interval: 30s
    scrapers:
      cpu:
      memory:
      disk:
      filesystem:
      network:
      load:
      paging:
      processes:
  journald:
    units:
      - ssh
      - systemd-networkd
    priority: info

processors:
  memory_limiter:
    check_interval: 1s
    limit_percentage: 80
    spike_limit_percentage: 25
  resourcedetection:
    detectors: [env, system]
    system:
      hostname_sources: [os]
  batch:
    timeout: 5s
    send_batch_size: 1000

exporters:
  otlphttp/aiaxoniq:
    endpoint: ${env:OIQ_ENDPOINT}
    headers:
      X-License-Key: ${env:OIQ_LICENSE_KEY}
    compression: gzip

service:
  telemetry:
    metrics:
      readers:
        - pull:
            exporter:
              prometheus:
                host: 127.0.0.1
                port: 8888
  pipelines:
    metrics:
      receivers: [otlp, hostmetrics]
      processors: [memory_limiter, resourcedetection, batch]
      exporters: [otlphttp/aiaxoniq]
    traces:
      receivers: [otlp]
      processors: [memory_limiter, resourcedetection, batch]
      exporters: [otlphttp/aiaxoniq]
    logs:
      receivers: [otlp, journald]
      processors: [memory_limiter, resourcedetection, batch]
      exporters: [otlphttp/aiaxoniq]
EOF
```

<Note>
  **The OTLP receivers bind to `127.0.0.1`, not `0.0.0.0`.** On a VM, the things
  exporting to this collector are on the same machine, so loopback is sufficient
  and it means the collector is not an open OTLP relay reachable from your
  network. Change it only if you deliberately want other hosts to export here,
  and put a firewall in front of it if you do.
</Note>

`journald` collects system logs. Narrow `units` to what you actually want —
without a filter, a busy host will ship a great deal of noise you pay to store.
Remove the `journald` receiver from the `logs` pipeline if you do not want
system logs at all.

## 4. Start it

```bash theme={null}
sudo systemctl daemon-reload
sudo systemctl enable --now otelcol-contrib
sudo systemctl restart otelcol-contrib
```

**Expected output** from `systemctl status otelcol-contrib`:

```text theme={null}
● otelcol-contrib.service - OpenTelemetry Collector Contrib
     Loaded: loaded (/lib/systemd/system/otelcol-contrib.service; enabled)
     Active: active (running) since Thu 2026-07-30 17:16:35 UTC; 8s ago
   Main PID: 4812 (otelcol-contrib)
```

`active (running)` — and, critically, still running thirty seconds later. A
collector that fails on its config exits and systemd restarts it in a loop, which
reads as "running" if you look at exactly the wrong moment.

Confirm from the log:

```bash theme={null}
sudo journalctl -u otelcol-contrib -n 20 --no-pager
```

```text theme={null}
info  service@v0.115.0/service.go:238   Starting otelcol-contrib...  {"Version": "0.115.0"}
info  otlpreceiver@v0.115.0/otlp.go:112 Starting GRPC server  {"endpoint": "127.0.0.1:4317"}
info  otlpreceiver@v0.115.0/otlp.go:169 Starting HTTP server  {"endpoint": "127.0.0.1:4318"}
info  service@v0.115.0/service.go:261   Everything is ready. Begin running and processing data.
```

## 5. Point applications at it

Applications on this host export to loopback and carry no license key:

```bash theme={null}
export OTEL_EXPORTER_OTLP_ENDPOINT="http://127.0.0.1:4318"
export OTEL_EXPORTER_OTLP_PROTOCOL="http/protobuf"
export OTEL_SERVICE_NAME="checkout"
export OTEL_RESOURCE_ATTRIBUTES="deployment.environment=production"
```

For a service managed by systemd, put those in its own `EnvironmentFile` rather
than in a shell profile — a profile is not read by a unit.

## Verify

The collector publishes its own metrics on port `8888`. Three counters separate
the two things that can be wrong:

```bash theme={null}
curl -s localhost:8888/metrics | grep -E 'otelcol_(receiver_accepted|exporter_sent|exporter_send_failed)'
```

| Counter                          | Rising means                               |
| :------------------------------- | :----------------------------------------- |
| `otelcol_receiver_accepted_*`    | Your application is reaching the collector |
| `otelcol_exporter_sent_*`        | The collector is reaching aiAxonIQ         |
| `otelcol_exporter_send_failed_*` | It is trying and being refused             |

Accepted rising while sent stays flat is the classic signature of a wrong
endpoint or a missing key in the exporter block. It is worth checking before
anything else, because from inside your application everything looks fine.

**Expected output** within a minute of start, with host metrics flowing:

```text theme={null}
otelcol_receiver_accepted_metric_points{receiver="hostmetrics"} 486
otelcol_exporter_sent_metric_points{exporter="otlphttp/aiaxoniq"} 486
otelcol_exporter_send_failed_metric_points{exporter="otlphttp/aiaxoniq"} 0
```

Then open **Infrastructure** in the product, set the range to the last 15
minutes, and find this host by its hostname — `resourcedetection` sets it from
the OS.

## Troubleshooting

<AccordionGroup>
  <Accordion title="The service restarts in a loop" icon="rotate">
    A config error. The collector refuses to run on an invalid configuration
    rather than running degraded, and systemd restarts it.

    ```bash theme={null}
    sudo journalctl -u otelcol-contrib -n 50 --no-pager | grep -i error
    ```

    Validate before restarting:

    ```bash theme={null}
    otelcol-contrib validate --config=/etc/otelcol-contrib/config.yaml
    ```

    No output and exit code `0` means the config is valid.
  </Accordion>

  <Accordion title="'environment variable OIQ_ENDPOINT not found'" icon="circle-exclamation">
    The unit is not reading your environment file. `systemctl edit` writes a
    drop-in at `/etc/systemd/system/otelcol-contrib.service.d/override.conf` —
    confirm it exists and contains the `EnvironmentFile` line, then
    `sudo systemctl daemon-reload`.

    Exporting the variables in your shell does not reach a systemd unit.
  </Accordion>

  <Accordion title="No journald logs arrive" icon="file-lines">
    The collector user must be able to read the journal. The package normally
    handles this; where it has not, add the service user to the `systemd-journal`
    group and restart:

    ```bash theme={null}
    sudo usermod -aG systemd-journal otelcol-contrib
    sudo systemctl restart otelcol-contrib
    ```

    Also check your `units` filter actually matches units on this host —
    `systemctl list-units --type=service` shows the real names.
  </Accordion>

  <Accordion title="Host metrics are missing while traces arrive" icon="gauge">
    The `hostmetrics` receiver is in the `metrics` pipeline only. Confirm it is
    listed under `service.pipelines.metrics.receivers`; a receiver defined but
    not wired into a pipeline is silently inert — no error, no data.
  </Accordion>
</AccordionGroup>

<AccordionGroup>
  <Accordion title="The collector starts, then logs 'Exporting failed. Will retry'" icon="arrows-rotate">
    Read the `error` field on that line — it names the cause exactly.

    ```text theme={null}
    Exporting failed. Will retry the request after interval.
      {"kind": "exporter", "name": "otlphttp/aiaxoniq",
       "error": "failed to make an HTTP request: Post \"…/v1/metrics\":
                 dial tcp: lookup … : no such host", "interval": "5.2s"}
    ```

    * `no such host` — the endpoint hostname does not resolve. Check
      `$OIQ_ENDPOINT` against the value on **Get Started**.
    * `connection refused` — the host resolves but nothing is listening on that
      port.
    * `401 Unauthorized` — the key is missing, malformed or revoked.
    * `404` — the endpoint already ends in `/v1/…`. It must be the **base** URL;
      the collector appends the signal path itself.

    The collector retries with backoff and does not drop data while it retries,
    so a transient failure here is not a loss.
  </Accordion>

  <Accordion title="Nothing at all in the logs after 'Everything is ready'" icon="ear-listen">
    The collector is running and receiving nothing. That is an application-side
    problem, not a collector one — your services are not exporting to it.

    Check that your application's `OTEL_EXPORTER_OTLP_ENDPOINT` points at the
    collector's OTLP port (`4318` for HTTP, `4317` for gRPC), not at aiAxonIQ.
  </Accordion>

  <Accordion title="401 with a key you know is correct" icon="key">
    Two causes that are not about the key's value:

    * **A trailing newline.** A key read from a file created by a shell heredoc,
      or a Kubernetes Secret made with `--from-file`, carries the newline as
      part of the value. Use `--from-literal`, or `printf` rather than `echo`.
    * **A validation outage.** If the receiver cannot reach the service that
      validates keys it fails closed and returns the same `401`. A sudden `401`
      across every service at once, with a key you have not changed, is far more
      likely to be this. Check `$OIQ_ENDPOINT/health` first.
  </Accordion>

  <Accordion title="Config changes appear to do nothing" icon="file-pen">
    The collector reads its configuration only at startup. Restart it after any
    edit, and confirm the file you edited is the one mounted into the process —
    a bind mount pointing at a path that does not exist silently yields the
    image's default config rather than an error.
  </Accordion>
</AccordionGroup>

## Next

<CardGroup cols={3}>
  <Card title="Instrument your application" icon="code" href="/send-data/otel/zero-code">
    Zero-code setup per language.
  </Card>

  <Card title="Docker" icon="docker" href="/send-data/platforms/docker">
    For containerised workloads on this host.
  </Card>

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