Research date: 2026-08-29
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.
Grafana does not read arbitrary host files from a dashboard. It queries a data source. Loki is the purpose-built log data source:
logs visualization renders Loki log streams directly.| 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
job, service, or host.|= "text", | json, or | logfmt at query time.clocThis 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.
Accessed 2026-08-29.
loki.source.fileloki.source.journalloki.source.docker