English | [Français](README.fr.md) # Logstream A simple syslog sink: receives logs over UDP/TCP on port 514, stores them in [VictoriaLogs](https://docs.victoriametrics.com/victorialogs/), and serves a clean web interface (light/dark theme, live search, live view, color tags, English/French UI). ``` devices ──514 udp/tcp──▶ logstream (Go) ──HTTP batches──▶ VictoriaLogs ▲ └── SSE (live) ──▶ browser └──── API / UI ◀─────────┘ ``` ## Architecture ![Schéma logique de Logstream](docs/architecture.png) - **Ingestion**: syslog (UDP/TCP) and Docker container logs both go through `sink()` (reverse DNS on IP hosts, without holding up the listeners), then the `Store` queue, which sends them in batches to VictoriaLogs. When VictoriaLogs is unreachable, batches are kept on disk (`/data/spool`, up to `SPOOL_MAX_MB`) and sent again, oldest first, once it is back. - **Live view**: `sink()` also publishes each message to the `Hub`, which streams it to the browsers over SSE. - **Search**: the HTTP API turns the UI filters into LogsQL queries sent to VictoriaLogs. - **Timeline**: `/api/histogram` (`histogram.go`) counts the logs per interval and severity, aligned on the local time; the zoom bounds (`from`/`to`) also apply to the list and the export. - **State**: tags and source settings live in `/data` (`logstream-data` volume); the logs themselves in the `vlogs-data` volume. The editable source of the diagram is [`docs/architecture.excalidraw`](docs/architecture.excalidraw) (open it on [excalidraw.com](https://excalidraw.com)). ## Getting started ```bash cp .env.example .env # optional docker compose up -d --build ./tools/send-test-logs.sh # sends 100 test messages ``` Then open . ## Sending logs - **rsyslog** (Linux): add `*.* @SERVER_IP:514` (UDP) or `*.* @@SERVER_IP:514` (TCP) to `/etc/rsyslog.d/90-logstream.conf`, then run `systemctl restart rsyslog`. - **Network gear, NAS, firewalls**: set the server IP and port 514 in their "remote syslog" settings. - **Manual test**: `logger -n 127.0.0.1 -P 514 -d "hello error"` (util-linux) or `echo "<14>test ok" | nc -u -w1 127.0.0.1 514`. **Settings > Log sources > Syslog** turns syslog reception on or off and chooses the protocols (UDP, TCP) without restarting; the choice is saved in `/data/syslog.json`. It also shows the listening state (and the error if the port is already in use). The port itself is published by docker-compose: change `SYSLOG_PORT` in `.env`, then run `docker compose up -d`. Supported formats: RFC 3164 (BSD) and RFC 5424. Over TCP, both "one message per line" and "octet counting" (RFC 6587) framing are accepted. ## Search **Simple mode** (default): each word is matched as a case-insensitive substring of the message, host and app. Words are combined with AND. | Input | Meaning | |---|---| | `error disk` | contains "error" **and** "disk" | | `"disk full"` | contains the exact phrase | | `error -timeout` | contains "error" but not "timeout" | **LogsQL mode** (`Simple` / `LogsQL` button): the full VictoriaLogs query language, e.g. `error AND host:web-01`, `app:~"ssh|nginx"`, or `* | stats by (host) count()`. The live view is disabled in this mode. Each row shows, from left to right: the **reception time** (server clock), the timestamp found in the message itself (`msg_time`), severity, host, app, codes of the tags found and message. Click a host or an app to filter on it. Logs are indexed, searched and sorted by **reception time**: devices with a wrong clock (e.g. access points whose NTP fails) still show up in the right time range. Logs stored by versions before this change are indexed by their message timestamp; purge them from Settings to start clean. Severity badges `err`/`crit` and `warning` use the colors of the `error` and `warning` tags. Shortcuts: `/` focuses the search box, `Esc` clears it. Clicking a row shows all its fields. The column headers stay visible while scrolling. Drag the edge of a header (received, message time, severity, host, app) to resize the column, double-click it to go back to the automatic width; the message takes the remaining space. Widths are remembered by the browser (**Settings > Appearance > Reset column widths** restores them all). On phones the list keeps its two-line layout without columns. ## Stream and Time range modes The first control of the filter bar switches between two display modes: - **Stream**: the latest logs over a sliding duration (5 min to 30 days, or all), with the live view. - **Time range**: the logs between a start and an end date, typed in the time zone chosen in Settings. ◀ and ▶ move to the previous or next range of the same length, the magnifier doubles it around its middle. The live view pauses; the mode and the range are kept across reloads. ## Timeline The timeline above the list shows the volume of logs per interval, counted by **reception time** on the server clock (so it matches the times shown in the rows). - Hover an interval: its bounds, total and detail per severity. - Click a bar to show that interval in Time range mode, or drag across several bars to show the selection; "× Back to stream" returns to Stream mode. The list, the counters and the CSV export follow the range. - In live mode the last interval grows as messages arrive, and the timeline reloads at each new interval. Nothing is refreshed while the browser tab is hidden; it catches up when shown again. **Settings > Appearance > Timeline** (remembered per browser): scale (linear, √ by default, log), height (S/M/L: 40/80/120 px), color (stacked by severity, intensity compared with the median of the window: calm, burst above 3×, anomaly above 10×, or none), bars or area, division (automatic, about 100 intervals, or fixed: 1 s, 10 s, 1 min, 5 min, 1 h, 1 day) and refresh (off, 5 s, 15 s, 30 s, 1 min, or at each new interval). A fixed division that would exceed 300 intervals over the range is enlarged (shown as "enlarged"). Intervals are aligned on the local time of the time zone chosen in Settings (days start at local midnight). API: `curl 'localhost:8080/api/histogram?range=24h&step=auto&tz=Europe/Paris'` returns `step` (ms), `start` (ms), `count`, `now` (server clock) and the non-empty `buckets` (`i` = interval index, `n` = total, `sev` = count per severity). Every log endpoint also accepts `from` / `to` (Unix milliseconds) instead of `range`. ## Docker container logs Logstream also collects the logs of the Docker containers running on the machine where it is installed (`DOCKER_LOGS=on`, the default in `docker-compose.yml`). They are searched, filtered, colored and exported like syslog messages: - **host** is the Docker host name, **app** the compose service (or the container name), and each log also carries `container`, `container_id`, `image`, `compose_project`, `compose_service` and `stream` (stdout/stderr), visible in the row details. - The **Source** filter shows only syslog or only Docker logs; Docker rows have a small cube before the app name, colored like its compose project. - The severity comes from the line itself when the application writes it: JSON (`"level":"error"`), logfmt (`level=warn`), `[ERROR]`, or an upper-case level word at the start of the line (`ERROR`, `WARN`…). Otherwise it is `info`. Terminal color codes are removed. - **Settings > Log sources** shows one label per container (`project/service`) in a single field: followed containers in color, then not followed ones in grey; a click switches a label. The color identifies the compose project, and Docker app names in the log list use the same color. Stopped containers are hidden by default ("Show stopped containers" displays them, dashed, and they stay editable). A filter box and "Enable all" / "Disable all" (applied to the labels shown) help with many containers. New containers are followed automatically unless that option is turned off. Choices are saved per compose service (or container name) in `/data/docker.json`, so they survive re-creations. - Logstream remembers the position of the last line stored for each container (`/data/docker-state.json`): after a restart it resumes without losing lines. The position only moves once a line is in VictoriaLogs or in the disk buffer, and a full queue slows the reading down instead of dropping lines. A container seen for the first time is read from `DOCKER_BACKFILL` ago (1 hour by default). - Logstream itself and the proxy below are never collected; add the label `logstream.exclude=true` to any other container to exclude it for good. **Security**: access to the Docker socket is equivalent to root on the machine. Logstream therefore goes through [docker-socket-proxy](https://github.com/Tecnativa/docker-socket-proxy), which only lets through listing containers, reading logs, events and engine info (`GET` only). ## Host system logs Logstream can also collect the system logs of the machine hosting the stack, without configuring anything on the host. The source is **off by default**: turn it on in **Settings > Log sources > Host system logs** (saved in `/data/hostlogs.json`). - `docker-compose.yml` mounts `/var/log` and `/run/log/journal` read-only under `/host`. - When the host runs systemd, Logstream reads the **systemd journal** files directly (no `journalctl` needed in the image): **host** is the machine name, **app** the program (`SYSLOG_IDENTIFIER`), severity and facility come from the journal, and the systemd unit is kept in `unit`. Otherwise it follows the text files of `/var/log` (`syslog`, `messages`, `*.log`), parsed like syslog lines, with the file name in `log_file`. - These logs have the `host` source: the **Source** filter shows them alone. - When the source is turned on, the last hour is read first (`HOST_LOGS_BACKFILL`); the position reached is saved in `/data/hostlogs-state.json`, so a restart neither loses nor duplicates entries. - **Permissions**: the container runs as an unprivileged user and gets the `adm` group (gid 4), which can read the journal and `/var/log` on Debian and Ubuntu. On other systems, set `HOST_LOGS_GID` in `.env` to the gid of `systemd-journal` (`getent group systemd-journal | cut -d: -f3`). Settings > Log sources shows a clear message when access is denied. - Limits: journal fields compressed by journald (messages longer than 512 bytes, compressed with zstd/lz4/xz) cannot be decoded without extra libraries; they are counted in Settings and shown as "(compressed journal entry)". Rotated or compressed text files (`*.1`, `*.gz`) are not read. ## Kernel messages (dmesg) Logstream can collect the kernel ring buffer of the host (what `dmesg` shows: hardware, drivers, OOM killer, firewall…) by reading `/dev/kmsg`. The source is **off by default**: turn it on in **Settings > Log sources > Kernel messages (dmesg)** (saved in `/data/dmesg.json`). - **Device**: the container does not see `/dev/kmsg` by default. Uncomment the `devices` section of `docker-compose.yml` (`/dev/kmsg:/dev/kmsg:r`, read-only), then `docker compose up -d`. - **Permissions**: when the host has `kernel.dmesg_restrict=1` (the default on Debian and Ubuntu), only processes with the `CAP_SYSLOG` capability can read the kernel log, and the container runs as an unprivileged user. Either: - allow it on the host: `sysctl -w kernel.dmesg_restrict=0`, kept across reboots with `echo kernel.dmesg_restrict=0 | sudo tee /etc/sysctl.d/50-dmesg.conf` (every user of the host can then run `dmesg`); this setting cannot be changed from a container; - or run Logstream as root with the `SYSLOG` capability: uncomment `user: "0:0"` and `cap_add: [SYSLOG]` in `docker-compose.yml`. Settings > Log sources says when the device is missing or access is denied; the rest of Logstream keeps working and the device is tried again every 10 seconds. - **Fields**: **app** is `kernel` (or the program name for messages written by programs such as `systemd[1]: …`), severity and facility come from the record, **host** is `localhost`, `subsystem` and `device` are kept when the kernel gives them. These logs have the `kernel` source (`proto` `kmsg`): the **Source** filter shows them alone. - **Time**: records carry the time since boot; Logstream turns it into a date using the boot time (`/proc/uptime`). As with `dmesg -T`, the time can be early after the host was suspended. - When the source is turned on, the last hour is read first (`DMESG_BACKFILL`, `0` = only new messages). The sequence number of the last message and the boot ID are saved in `/data/dmesg-state.json`: after a restart of Logstream, reading resumes after the last message (no duplicates); after a reboot of the host, the new buffer is read from `DMESG_BACKFILL` ago. Messages overwritten in the kernel buffer before being read are counted in Settings. ## Linux audit (auditd) Logstream can follow the log of the Linux audit daemon (`/var/log/audit/audit.log`): logins, `sudo`, watched files, syscalls matched by your `auditctl` rules… The source is **off by default**: turn it on in **Settings > Log sources > Linux audit (auditd)**, where the file path can also be changed (saved in `/data/audit.json`; `AUDIT_LOG` sets the default path). - **Mount**: uncomment `/var/log/audit:/var/log/audit:ro` in `docker-compose.yml`, then `docker compose up -d`. (`/var/log` is already mounted under `/host` for the host system logs, so `/host/var/log/audit/audit.log` also works as the path, without a new volume.) - **Permissions** (host side): auditd writes its log as `root`, mode `0600`, in a `0700` directory, and Logstream runs as UID `10001`. The reliable way is the group the container already gets for the host logs (`HOST_LOGS_GID`, `adm` by default): ```bash sudo sed -i 's/^log_group.*/log_group = adm/' /etc/audit/auditd.conf sudo chgrp adm /var/log/audit && sudo chmod 750 /var/log/audit sudo service auditd restart # systemctl refuses to restart auditd on some distributions ``` auditd then creates every rotated file with mode `0640` and group `adm`. An ACL also works (`sudo setfacl -m u:10001:rx /var/log/audit && sudo setfacl -m u:10001:r /var/log/audit/audit.log`) but must be applied again to each new file after a rotation. Settings > Log sources says when the file is missing or access is denied; it is tried again every second. - **Events**: the records of one event (`SYSCALL`, `EXECVE`, `CWD`, `PATH`, `PROCTITLE`…) share the same `msg=audit(time:serial)` and become **one** log, once no record came for 2 seconds. The message is a summary — who, what, result — such as `cedric (as root): exec apt install nginx [exec] → success` or `bob: PAM:authentication via /usr/sbin/sshd from 203.0.113.9 on ssh → failed`; the raw records are kept in `audit_raw` and shown under the summary (folded in compact density until the line is opened). Hexadecimal values (`proctitle`, `cmd`, arguments with spaces) are decoded. - **Fields**: `audit_type` (type of the first record, also used as **app**), `audit_serial`, `auid` (login user ID), `user` (its name, or the uid when it cannot be resolved), `exe`, `res` (`success` / `failed`, from `res=` or `success=`) and `key` (rule key). Severity is `warning` when the result is a failure, `info` otherwise; facility `authpriv`; source `audit`. In the list, audit lines have a violet left edge, a light tint and an **AUDIT** badge before the app; the **Source** filter shows them alone. - **Names**: `user` comes from the enriched format (`log_format = ENRICHED`, the default of recent auditd versions), else from the host's `/etc/passwd` when it is mounted as `/host/etc/passwd`. **host** is the `node=` of the records (`name_format = hostname` in `auditd.conf`), else `AUDIT_HOST`, else `/host/etc/hostname`, else `localhost`. - When the source is turned on, the last hour is read first (`AUDIT_BACKFILL`, `0` = only new events). The inode and an offset before which every event was sent are saved in `/data/audit-state.json`: after a restart, reading resumes there; after a rotation by auditd, the end of `audit.log.1` is read before the new file. ## Log files (Traefik, nginx, applications…) Logstream can follow the log files of other programs: Traefik, nginx, Apache, or any application that writes to a file. Declare each file in **Settings > Log sources > Log files** (saved in `/data/files.json`, up to 50): a **name** (the **app** of its entries), the **path** as mounted in the container, a **format**, an optional **host** (default: `/host/etc/hostname`, else `localhost`) and a switch. - **Mount** the directory read-only, e.g. in `docker-compose.yml`: `- /var/log/traefik:/logs/traefik:ro`, then `docker compose up -d`. Files under `/var/log` are already reachable as `/host/var/log/…`. The files must be readable by UID `10001` or the group of `HOST_LOGS_GID` (`adm` by default); Settings > Log sources says when no file matches or access is denied. - **Path**: a file, or a pattern with `*`, `?` or `[…]`: `/logs/traefik/*.log` follows every `.log` file of the directory, also those created later (200 files at most per pattern). Rotated copies (`.1`, `.2.gz`…) are left out. - **Formats**: - *Plain text*: one entry per line; the level is detected as for Docker logs, a timestamp at the start of the line (ISO 8601, `2006/01/02 15:04:05`, `[02/Jan/2006:15:04:05 +0200]`, `Jan 2 15:04:05`) becomes the message time and is removed from the message. Indented lines (stack traces) are added to the previous entry. - *JSON lines*: `msg` / `message`, `time` / `ts` / `@timestamp` (text or Unix time) and `level` (word or number, pino/bunyan included) are recognized; the other keys become fields, nested objects with dotted names (`req.id`). - *logfmt*: `key=value` pairs, as in Traefik's default logs or Go programs; same keys as JSON. - *Access log (CLF)*: Common / Combined Log Format of nginx and Apache, and Traefik's access log in this format (router, server URL and duration included). Fields `client_ip`, `http_method`, `http_path`, `http_status`, `http_size`, `user_agent`, `http_referer`, `router`, `backend`, `duration_ms`. - *Traefik access (JSON)*: the JSON access log of Traefik (`accessLog.format: json`), summarized as `GET app.example.com/ 200 · 12.5 ms · app@docker · 203.0.113.9`, every key in the fields. Traefik's own logs in the same file are read as JSON lines. - *Regular expression* (RE2 / Go syntax): the named groups `time`, `level`, `msg`, `host` and `app` fill these fields, `status` gives the level of an access log, the other groups become fields, e.g. `^(?P