Minimal logs in Grafana

Research date: 2026-08-29

Short answer

Use Grafana Loki + Grafana Alloy. Loki stores logs; Alloy tails the existing log files and forwards them; Grafana's built-in Loki data source displays them. Do not add Elasticsearch, Prometheus, Kafka, Kubernetes, or Promtail for this use case.

For the repository examined here, the smallest private setup is:

existing Grafana  <-  Loki  <-  Alloy  <-  ~/.local/state/job-logs/*.log

Your Grafana setup already uses Podman and file provisioning. Add one Loki container, one Alloy container, one Loki data-source entry, and one logs panel.

If avoiding server maintenance is more important than keeping logs local, use Grafana Cloud Logs + Alloy instead. That removes the local Loki process and storage, but sends logs to a hosted service.

What Grafana needs

Grafana does not read arbitrary host files from a dashboard. It queries a data source. Loki is the purpose-built log data source:

Options compared

Option Extra components Best fit Suckless assessment
Loki + Alloy, local filesystem 2 Private, one host, existing Grafana Recommended local choice
Grafana Cloud Logs + Alloy 1 local agent Fewest operations; hosted storage is acceptable Recommended hosted choice
Loki Docker logging driver Loki + Docker plugin Docker-only hosts Fewer config files, but weaker failure behavior and less flexible
Application sends HTTP directly to Loki Loki only One small application you control Smallest runtime, but couples application code to logging and misses host logs
Fluent Bit or OpenTelemetry Collector Collector + Loki Already deployed in the environment Do not add a second collector just for this
Promtail Promtail + Loki Existing installations only Do not start new deployments: EOL on 2026-03-02

Grafana lists Alloy as its primary log-sending method. Fluent Bit is sensible when it is already present. The Docker driver is maintained as a third-party client and Grafana documents known blocking, buffering, and possible log-loss behavior when Loki is unavailable.

Recommended private setup

This example assumes Grafana is already running. It keeps Loki single-binary, single-tenant, and local. Filesystem storage is appropriate for a low-volume personal setup, development, or proof of concept; it is not replicated or production-safe.

1. Loki configuration

Save as loki-config.yaml:

auth_enabled: false

server:
  http_listen_port: 3100

common:
  path_prefix: /loki
  replication_factor: 1
  ring:
    kvstore:
      store: inmemory

schema_config:
  configs:
    - from: "2024-01-01"
      store: tsdb
      object_store: filesystem
      schema: v13
      index:
        prefix: index_
        period: 24h

storage_config:
  filesystem:
    directory: /loki/chunks

# Bound the disk used by personal logs.
limits_config:
  retention_period: 168h # 7 days

compactor:
  working_directory: /loki/retention
  retention_enabled: true
  delete_request_store: filesystem

auth_enabled: false is only suitable when Loki is kept on a private container network or bound to localhost. Do not expose this endpoint to an untrusted network without authentication and TLS.

Seven days is an example, not a requirement. Set it to the shortest period that is useful to you. Loki's Compactor is required for retention; without it, logs live forever.

2. Alloy configuration for the existing job logs

Your scheduled-job runner writes files below $XDG_STATE_HOME/job-logs. Mount that directory into Alloy as /logs and save this as alloy.alloy:

loki.source.file "job_logs" {
  targets = [
    { "__path__" = "/logs/*.log", "job" = "job_logs" },
  ]
  forward_to    = [loki.write.local.receiver]
  tail_from_end = true

  file_match {
    enabled     = true
    sync_period = "10s"
  }
}

loki.write "local" {
  endpoint {
    url = "http://loki:3100/loki/api/v1/push"
  }
}

This deliberately has no parsing pipeline. The useful label is the bounded job=job_logs; the filename is added automatically. Search the message text with LogQL instead of turning every message field into a label.

3. Run the two containers with Podman

The current Grafana service uses Podman, so these commands avoid Compose. Pin versions rather than using latest; the tags below were current when this report was written.

podman network create observability 2>/dev/null || :
podman volume create loki-data >/dev/null
podman volume create alloy-data >/dev/null

podman run -d --name loki --network observability \
  -p 127.0.0.1:3100:3100 \
  -v loki-data:/loki \
  -v "$PWD/loki-config.yaml:/etc/loki/config.yaml:ro" \
  docker.io/grafana/loki:3.7.7 \
  -config.file=/etc/loki/config.yaml

job_logs=${XDG_STATE_HOME:-$HOME/.local/state}/job-logs
podman run -d --name alloy --network observability \
  -v "$job_logs:/logs:ro" \
  -v "$PWD/alloy.alloy:/etc/alloy/config.alloy:ro" \
  -v alloy-data:/var/lib/alloy/data \
  docker.io/grafana/alloy:v1.19.2 \
  run --storage.path=/var/lib/alloy/data /etc/alloy/config.alloy

Attach the existing grafana container to the same observability network, or add --network observability to its existing podman run command. Then Grafana can reach Loki at http://loki:3100.

podman network connect observability grafana

A one-time network connect is lost if the runit service recreates the Grafana container. For this repository, put --network observability in .config/sv/grafana/run instead.

4. Provision the Loki data source

Append this item under the existing datasources: list in .config/grafana/provisioning/datasources/sqlite.yaml, or place it in another data-source provisioning file:

  - name: Loki
    uid: loki
    type: loki
    access: proxy
    url: http://loki:3100
    isDefault: false
    editable: false

The existing Grafana provider already scans .config/grafana/dashboards, so no new dashboard provider is necessary.

5. Add the dashboard panel

In Grafana, add a Logs visualization, select the Loki data source, and use:

{job="job_logs"}

Useful minimal filters:

{job="job_logs"} |= "error"
{job="job_logs"} |= "failed"

For a provisioned dashboard, put a panel like this inside the dashboard's panels array:

{
  "datasource": {"type": "loki", "uid": "loki"},
  "gridPos": {"h": 12, "w": 24, "x": 0, "y": 0},
  "id": 1,
  "options": {
    "showLabels": true,
    "showTime": true,
    "sortOrder": "Descending",
    "wrapLogMessage": true
  },
  "targets": [
    {"expr": "{job=\"job_logs\"}", "refId": "A"}
  ],
  "title": "Job logs",
  "type": "logs"
}

This repository sets allowUiUpdates: false, so a panel created only in the UI is not the durable source of truth. Export it or edit the dashboard JSON.

6. Verify without extra tools

curl -fsS http://127.0.0.1:3100/ready
printf '%s\n' "test $(date -Is)" >> "$job_logs/test.log"

Open Grafana Explore and run {job="job_logs"}. Remove the test file afterwards if desired.

Other collection modes

Systemd journal

Use this instead of loki.source.file when the logs are in journald:

loki.source.journal "systemd" {
  matches    = "_SYSTEMD_UNIT=sshd.service"
  labels     = {"job" = "sshd"}
  forward_to = [loki.write.local.receiver]
}

The Alloy service user must be in the adm and systemd-journal groups:

sudo usermod -aG adm,systemd-journal alloy
sudo systemctl restart alloy

For this repository's runit job logs, file tailing is simpler than adding journal access.

Docker containers

Alloy can discover and read Docker container logs without changing every application:

discovery.docker "containers" {
  host = "unix:///var/run/docker.sock"
}

loki.source.docker "containers" {
  host       = "unix:///var/run/docker.sock"
  targets    = discovery.docker.containers.targets
  labels     = {"job" = "docker"}
  forward_to = [loki.write.local.receiver]
}

This requires mounting the Docker socket into Alloy. With Podman, use the equivalent Podman socket and component endpoint only if that is already part of the environment; do not add it just for one log directory.

Grafana Cloud Logs

If hosted storage is acceptable, keep the file source and replace the local writer:

loki.write "cloud" {
  endpoint {
    url = "https://logs-xxx.grafana.net/loki/api/v1/push"
    basic_auth {
      username = sys.env("LOKI_USERNAME")
      password = sys.env("GRAFANA_CLOUD_API_KEY")
    }
  }
}

Change the file source's forward_to to [loki.write.cloud.receiver]. Keep credentials in environment variables, not in the Alloy file. This is the fewest operational components, but introduces a hosted-service dependency and sends logs outside the machine.

Direct application HTTP push

For one small application, no collector is necessary. Loki accepts JSON at /loki/api/v1/push:

curl -fsS -X POST http://127.0.0.1:3100/loki/api/v1/push \
  -H 'Content-Type: application/json' \
  --data-raw '{"streams":[{"stream":{"job":"manual"},"values":[["'"$(date +%s%N)'"","hello from the app"]]}]}'

This is useful for a test or a deliberately tiny program. For real applications, batching, retries, backpressure, and error handling become application responsibilities. It also cannot collect system, container, or third-party service logs.

Docker Loki logging driver

For Docker-only environments, the driver can remove Alloy:

docker plugin install grafana/loki-docker-driver:3.7.0-amd64 \
  --alias loki --grant-all-permissions

docker run --log-driver=loki \
  --log-opt loki-url=http://loki:3100/loki/api/v1/push \
  your-image

It is concise, but it is not the preferred choice here: it is a third-party client, Docker-specific, unavailable for Windows, and can block the Docker daemon or drop logs when Loki is unavailable. Grafana recommends Alloy's Docker source instead.

Minimal operating rules

  1. Start with one or two stable labels: job, service, or host.
  2. Do not label user IDs, request IDs, IP addresses, timestamps, or arbitrary URLs. Loki warns that high-cardinality labels create many streams and hurt performance.
  3. Keep raw log lines unparsed until a query needs parsing. Use |= "text", | json, or | logfmt at query time.
  4. Set retention deliberately. Filesystem Loki is simple but unreplicated; back up the volume if the logs matter.
  5. Keep Loki private. The local example disables authentication for simplicity and assumes a trusted local network.
  6. Do not install Promtail for a new setup. It is EOL; use Alloy.

Verdict

Actual tool source size measured with cloc

This is tool source size, not configuration size and not compiled binary size. Counts use the code column from cloc 2.11: blank lines and comments are excluded. Tests and checked-in generated files are included. Vendored dependencies and common build/dependency directories are excluded with --exclude-dir=vendor,node_modules,dist,build,target,public.

The repositories were checked out at these releases on 2026-08-29:

Tool Source release Recognized files Code lines Blank Comments
Grafana OSS grafana/grafana v13.2.0 12,162 2,600,837 285,429 149,584
Grafana Loki grafana/loki v3.7.7 3,217 1,087,871 166,802 46,024
Grafana Alloy grafana/alloy v1.19.2 3,274 426,110 73,666 29,645
Podman containers/podman v6.1.0 2,283 273,782 53,688 55,575
OpenTelemetry Collector core open-telemetry/opentelemetry-collector v0.159.0 2,370 237,701 39,776 21,231
Fluent Bit fluent/fluent-bit v5.1.1 9,336 2,839,519 464,010 494,951
Loki Docker driver* Loki v3.7.7, clients/cmd/docker-driver 14 2,288 213 42

*The Docker driver is a small subdirectory, not an independent repository. Its count includes its checked-in generated protobuf file and tests; its binary also uses Loki client libraries and external dependencies.

The direct HTTP option has no separate collector. Grafana Cloud Logs is a managed service, so its server source is not published for counting. curl and systemd are existing host utilities, not additional log backends. Promtail is not counted as a candidate because Grafana declared it EOL on 2026-03-02 and recommends migration to Alloy.

The reproducible command was:

cloc --git \
  --exclude-dir=vendor,node_modules,dist,build,target,public \
  /path/to/checked-out-repository

For Alloy's .alloy files, no forced language was needed because this count is repository-wide and the source is mostly Go. For the small configuration snippets earlier in the report, language forcing was only a counting workaround; those previous numbers should not be confused with this table.

What the source-size numbers mean

Sources

Accessed 2026-08-29.