# Cloudflare Tunnel Logs

> Send Cloudflare Tunnel (cloudflared) logs to Last9 via the OpenTelemetry Collector journald receiver with correct severity mapping.

Source: https://last9.io/docs/integrations/observability/cloudflare-tunnel-logs/

Send logs from the Cloudflare Tunnel daemon (`cloudflared`) to Last9 using the OpenTelemetry Collector's journald receiver.

## Prerequisites

1. Create a Last9 account by following [Getting Started](/docs/onboard/).
2. Keep the following from the [Integrations](https://app.last9.io/integrations?integration=OpenTelemetry) page:
   - `$last9_otlp_endpoint` — Last9's OTLP endpoint
   - `$last9_basic_auth_header` — OpenTelemetry Basic authorization header
3. `cloudflared` installed and running as a systemd service on your host.

Verify cloudflared is active before proceeding:

```bash
systemctl status cloudflared
journalctl -u cloudflared -n 10 --no-pager
```

## Install OpenTelemetry Collector

Install `otelcol-contrib` on the same host as `cloudflared`:

```bash
sudo apt-get update && sudo apt-get install -y wget
wget https://github.com/open-telemetry/opentelemetry-collector-releases/releases/download/v0.120.0/otelcol-contrib_0.120.0_linux_amd64.deb
sudo dpkg -i otelcol-contrib_0.120.0_linux_amd64.deb
```

More installation options: [OpenTelemetry Collector installation](https://opentelemetry.io/docs/collector/installation/).

## Configuration

Edit `/etc/otelcol-contrib/config.yaml`:

```yaml
receivers:
  journald:
    directory: /var/log/journal
    units:
      - cloudflared
    priority: info
    operators:
      # Tag logs with tunnel identity
      - type: add
        field: attributes["connector.id"]
        value: "<your_connector_id>"
      - type: add
        field: attributes["tunnel.name"]
        value: "<your_tunnel_name>"

      # cloudflared sets PRIORITY=6 for all log levels regardless of actual severity.
      # Parse the level token from the message body instead (INF/WRN/ERR/DBG/FTL).
      - type: regex_parser
        parse_from: body["MESSAGE"]
        regex: '^\d{4}-\d{2}-\d{2}T[\d:Z]+ (?P<log_level>[A-Z]{2,4})'
        on_error: send
      - type: severity_parser
        parse_from: attributes.log_level
        mapping:
          debug: DBG
          info: INF
          warn: WRN
          error: ERR
          fatal: FTL

processors:
  resource:
    attributes:
      - key: service.name
        value: "cloudflare-tunnel"
        action: upsert
      - key: service.namespace
        value: "<your_namespace>"
        action: upsert
  batch:
    timeout: 5s
    send_batch_size: 200

exporters:
  otlphttp/last9:
    endpoint: "$last9_otlp_endpoint"
    headers:
      Authorization: "$last9_basic_auth_header"

service:
  pipelines:
    logs:
      receivers: [journald]
      processors: [resource, batch]
      exporters: [otlphttp/last9]
```

### Why parse severity from the message body?

`cloudflared` does not set the journald `PRIORITY` field correctly — it emits `PRIORITY=6` (INFO) for all log entries including warnings and errors. Severity must be extracted from the level token embedded in the message body:

```
2026-04-22T01:45:13Z ERR failed to accept incoming stream requests error="..."
                     ^^^
                     actual level token
```

The `regex_parser` operator extracts this token and `severity_parser` maps it to the OTel severity scale:

| cloudflared token | OTel severity |
| ----------------- | ------------- |
| `DBG`             | Debug         |
| `INF`             | Info          |
| `WRN`             | Warn          |
| `ERR`             | Error         |
| `FTL`             | Fatal         |

## Start the collector

```bash
sudo systemctl start otelcol-contrib
sudo systemctl enable otelcol-contrib
sudo systemctl status otelcol-contrib
```

Check collector logs:

```bash
sudo journalctl -u otelcol-contrib -f
```

## Verification

Visit [Log Explorer](https://app.last9.io/logs) and filter by `service.name = cloudflare-tunnel`. You should see logs with populated severity levels (Info, Warn, Error).

## Running with Docker

If you prefer to run the collector in Docker instead of installing it directly, mount the host's journald directories:

```yaml
services:
  otelcol:
    image: otel/opentelemetry-collector-contrib:0.120.0
    command: ["--config=/etc/otelcol-contrib/config.yaml"]
    volumes:
      - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml:ro
      - /run/log/journal:/run/log/journal:ro
      - /var/log/journal:/var/log/journal:ro
      - /etc/machine-id:/etc/machine-id:ro
    group_add:
      - "systemd-journal"
    restart: unless-stopped
```

:::note
The container needs to join the `systemd-journal` group to read journald logs. Get the GID with `getent group systemd-journal` on your host and ensure it matches.
:::

A complete working example is available at [l9_otel_examples/cloudflare-tunnel](https://github.com/last9/l9_otel_examples/tree/main/cloudflare-tunnel).

---

## Troubleshooting

**No logs in Last9**

- Confirm `cloudflared` is writing to journald: `journalctl -u cloudflared -n 5`
- Check collector can read the unit: add `debug` exporter temporarily to see what's being received

**All logs appear as INFO severity**

- Verify cloudflared log format matches the regex: `journalctl -u cloudflared -n 5 --output=cat`
- Expected format: `2026-01-01T00:00:00Z ERR message text`

Please get in touch with us on [Discord](https://discord.com/invite/Q3p2EEucx9) or [Email](mailto:support@last9.io) if you have any questions.
