Self-Hosted Grafana Observability Stack

This guide walks you through setting up a complete, self-hosted observability stack where Qtap events flow to Grafana via Loki and full HTTP payloads stay in your own S3-compatible object storage. Every byte of captured data — metadata and payloads — remains inside your infrastructure.

Architecture

┌──────────────────────────────────────────────────────────────────┐
│                        YOUR INFRASTRUCTURE                       │
│                                                                  │
│   ┌───────┐                                                      │
│   │ Qtap  │──── Events (metadata) ───► OTel Collector (:4317)    │
│   │ Agent │                               │                      │
│   │       │                               ▼                      │
│   │       │                           Loki (:3100)               │
│   │       │                               │                      │
│   │       │                               ▼                      │
│   │       │                           Grafana (:3000)            │
│   │       │                               ▲                      │
│   │       │                               │ click artifact URL   │
│   │       │                               ▼                      │
│   │       │                       nginx proxy (:3904)            │
│   │       │                               │                      │
│   │       │                               ▼                      │
│   │       │── Objects (payloads) ──► Garage S3 (:3900)           │
│   └───────┘                                                      │
└──────────────────────────────────────────────────────────────────┘
Service
Role
Port

Qtap

eBPF agent — captures HTTP traffic at kernel level

Host network

OTel Collector

Receives OTLP logs from Qtap, forwards to Loki

4317 (gRPC)

Loki

Log aggregation and storage

3100

Grafana

Query, explore, and visualize events

3000

Garage

S3-compatible object storage for HTTP payloads

3900 (S3 API)

nginx

Proxy for anonymous read access to stored objects

3904

circle-info

This guide covers events and object linking — not traces. For network timing traces, see the OpenTelemetry Integration guide.

Prerequisites

Understanding the Two Data Paths

Qtap produces two distinct outputs. Understanding the split is key to this architecture:

Events
Objects

What

Lightweight metadata — method, URL, status, duration, process

HTTP transaction objects — metadata at summary level; headers and bodies at full level

Sensitivity

Low — safe to send anywhere

Varies — summary objects contain only metadata; full objects may contain API keys, tokens, PII

Storage

Loki (via OTel Collector)

Garage S3 (your infrastructure)

Volume

Every observed request

Every captured request (content varies by capture level)

The link between them: Qtap's access_url template embeds a clickable URL into each artifact_record event. When you find an interesting event in Grafana, you click the URL to fetch the complete HTTP transaction from your S3 storage.

Configuration Files

Create a project directory and add these files:

OTel Collector

The collector listens for gRPC on port 4317 (where Qtap sends events) and forwards them to Loki's OTLP endpoint. Since the collector runs with network_mode: host, it reaches Loki at localhost:3100.

Loki

Key settings: allow_structured_metadata: true lets Loki store Qtap's structured attributes (method, status, host, etc.) as queryable fields. Retention is set to 7 days (168h).

Garage (S3-Compatible Object Storage)

Garage provides the S3 API on port 3900 (where Qtap writes payloads), a web endpoint on 3902 (for anonymous reads), and an admin API on 3903 (for bucket management).

Nginx Proxy

The nginx proxy translates requests like http://localhost:3904/qpoint/<DIGEST> into Garage web requests with the correct virtual-host Host header. This avoids needing wildcard DNS for Garage's subdomain-based routing.

Grafana Datasource

Qtap

This configuration captures all egress HTTP traffic at summary level (metadata only — no headers or bodies), and automatically escalates to full capture for any 4xx or 5xx response. Full captures store complete request/response headers in Garage S3. Both levels emit artifact_record events with clickable URLs pointing to the stored objects.

Docker Compose

circle-info

The OTel Collector uses network_mode: host so that Qtap (running on the host) can reach it at localhost:4317. Because it shares the host network stack, it also reaches Loki at localhost:3100.

Running the Stack

Step 1: Start the Services

Verify all containers are running:

Step 2: Initialize Garage

Wait for Garage to be ready, then configure the cluster layout, create a bucket, and set up access credentials:

circle-exclamation

Step 3: Start Qtap

Step 4: Wait for Initialization

Qtap needs a few seconds to load eBPF programs and start capturing.

Step 5: Generate Test Traffic

Generate both a successful request and an error to see both data paths:

Step 6: Verify Data Is Flowing

Check OTel Collector is receiving data:

You should see log export messages.

Check Loki has events:

Check S3 has objects (from the error request):

Exploring Data in Grafana

Open http://localhost:3000arrow-up-right in your browser (default credentials: admin / admin).

Viewing Events

  1. Navigate to Explore (compass icon in the left sidebar)

  2. Select Loki as the datasource

  3. Enter a LogQL query:

  1. Click Run query

You should see Qtap events — both connection events (TCP connection metadata) and artifact_record events (HTTP transaction summaries, with full headers and bodies for requests matching capture rules).

Useful LogQL Queries

Query
Description

{service_name="qtap"}

All Qtap events

{service_name="qtap"} | json | event_type="artifact_record"

Only artifact records (objects stored in S3)

{service_name="qtap"} | json | response_status >= 400

Error responses

{service_name="qtap"} | json | request_host="httpbin.org"

Traffic to a specific host

{service_name="qtap"} | json | duration_ms > 1000

Slow requests (> 1s)

{service_name="qtap"} | json | process_exe="/usr/bin/curl"

Requests from curl

{service_name="qtap"} | json | direction="egress-external"

External egress traffic

Expanding Log Entries

Click on any log entry to expand it. You'll see structured attributes including:

  • request_method, request_host, request_scheme

  • response_status, duration_ms

  • process_exe, direction

  • For artifact records: digest, url, type

Object Linking — From Events to Full Payloads

This is the key capability of this stack: linking lightweight events in Grafana to complete HTTP transactions stored in your own S3.

How It Works

  1. Qtap captures a request — all capture levels store an object in Garage S3, keyed by its SHA1 digest. In our config, errors get full capture (with headers), while other traffic gets summary (metadata only)

  2. The HTTP transaction object is stored in Garage S3 as JSON

  3. Qtap emits an artifact_record event to the OTel Collector, which includes a url field pointing to the stored object

  4. The event flows through to Loki and appears in Grafana

  5. You click the URL to view the complete HTTP transaction

Walkthrough

1. Find an error event in Grafana

In Explore, query for artifact records:

2. Expand the log entry

Click on an artifact record event. Look for these fields:

3. Click the URL

The url field is a direct link to the stored object. Click it (or open it in a new tab) to see the complete HTTP transaction:

This is the full HTTP transaction — headers and metadata — stored entirely in your infrastructure.

circle-info

All capture levels generate artifact_record events and store objects in S3. The difference is content: summary objects contain only metadata (method, URL, status, duration), while headers and full objects include the complete request/response headers and bodies. The object linking walkthrough above is most useful for full captures where you can inspect the actual HTTP payload.

The access_url Template

The link between events and objects is configured in the Qtap object_stores section:

{{DIGEST}} is replaced with the SHA1 hash of the stored object. The resulting URL is embedded in every artifact_record event.

For production, replace localhost with the hostname or IP address that Grafana users can reach:

Cleanup

Next Steps

Last updated