tailon-ng

command module
v0.0.0-...-eea0568 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 14, 2026 License: Apache-2.0 Imports: 29 Imported by: 0

README

Tailon-ng screenshot

Tailon-ng

Apache License

This is a fork of gvalkov/tailon. It keeps the purpose — tail and grep your log files from the browser — but rebuilds the project around the Go standard library: two third-party dependencies (pure-Go xz/zstd decoders for rotated archives, nothing else), no JavaScript toolchain, and a single static binary.

Do one thing, but do it right. Tailon-ng tails and shows your log files from the browser. That's it. There is no dashboard builder, no ingestion pipeline, no query language, no agents and no config file to learn. What you get: new lines appear the moment they are written, search answers fast no matter how big the file, switching between files is instant, and logs from many hosts read as one chronological stream.

Reading, following and searching are all done natively in Go and in the browser: tailon-ng never shells out to tail, grep or any other tool. It is almost entirely Go standard library, the only third-party code is two pure-Go decompression libraries (ulikunitz/xz and klauspost/compress for zstd) used to read rotated archives, and ships as a single static binary.

tailon-ng has no built-in authentication. Anyone who can reach its port can read the files it serves, so bind it to localhost (-b 127.0.0.1:8080) or put it behind an authenticating reverse proxy.

How this fork differs from the original

The "original" here is gvalkov/tailon, which itself is a fork of tailon-legacy. Comparison:

Area gvalkov/tailon (upstream) This fork
Third-party dependencies Vue.js, SockJS, build-time tooling Two pure-Go decompression libraries (xz, zstd); everything else is standard library
Frontend Vue.js single-page app Framework-free vanilla HTML/CSS/JS
Live updates SockJS Server-Sent Events
Frontend assets produced by a code-generation/build step embedded with //go:embed, no build step
Releases GoReleaser a small release.sh + GitHub Actions
Tests Python integration tests Go unit tests (go test ./...)

Install

Install the latest release binary for your OS and architecture (Linux and macOS, Intel and Apple Silicon). The script installs to /usr/local/bin, or ~/.local/bin when that isn't writable:

curl -sL https://raw.githubusercontent.com/tbocek/tailon-ng/main/install.sh | bash

Or install from source with Go (1.26+):

go install github.com/tbocek/tailon-ng@latest

Prebuilt binaries are also attached to every entry on the releases page.

Docker

A multi-arch (amd64/arm64) image is published to the GitHub Container Registry on every release. Paths to serve are passed as arguments, exactly as on the command line:

docker run --rm -p 127.0.0.1:8080:8080 -v /var/log:/var/log:ro \
  ghcr.io/tbocek/tailon-ng:latest /var/log

:latest is the newest release; :N (e.g. :28) pins one — image tags mirror the plain-increment git tags (v1, v2, …). The image is built FROM distroless static — just the static binary, no shell or package manager. It runs as root so it can read root-owned system logs (such as /var/log/syslog) out of the box; to drop privileges when you only serve logs a non-root user can read, add --user (for example --user 65532, or --user 65532:4 to keep the owning group's read access).

The image is small. A pull downloads the compressed content, about 4.33 MB; the disk-usage figure is the unpacked image (both architectures, under the containerd image store):

IMAGE                         ID             DISK USAGE   CONTENT SIZE   EXTRA
ghcr.io/tbocek/tailon-ng:28   34285fe33ef8       19.3MB         4.33MB

Usage

Run tailon-ng with the files or directories you want to watch — each argument is a file, a directory (served recursively; new files are picked up as they appear), or a glob, where * matches within a directory and ** across them. Quote globs so tailon-ng expands them, not the shell — tailon-ng re-expands on every listing, so files created later are picked up. Then open http://localhost:8080:

tailon-ng /var/log/apache/access.log /var/log/apache/error.log /var/log/messages
tailon-ng /var/log/apache/ /var/log/nginx/
tailon-ng "/var/log/**.log"

The server itself is configured through its command-line flags, everything else is the web UI.

Three modes. tail follows live, like tail -f — the mode for groups and All files, whose streams merge in timestamp order. view shows a whole file; selecting a single file always opens it in view, and a view keeps following after its backlog loads, so for one file view is tail plus history. find searches on the server and shows the first matches from each file with a few lines of context; two selects adjust how many matches (up to 100 per file) and how much context (up to ±10 lines). Clicking a result opens the file's view at that line, with the query carried along and its matches already highlighted.

Search vs find. In tail and view, the input is a browser-side search that highlights and hides nothing: matching lines and text light up as you type, lines streaming in live match as they arrive, and Enter or the ▲▼ buttons step between matches. The split is labeled in the UI: search covers the shown lines, find scans the whole files on the server. A view's match counter bridges the two — it also shows the server-counted whole-file total, and clicking it continues the search on the server, listing up to 100 matches across the whole file, each jumping back into the view at its line.

Three editor-style toggles inside the input shape search and find alike: Aa match case, .* regular expression (off searches the literal text), ! invert (keep the lines that do not match, like grep -v). The ☰ menu holds the remaining settings — wrap lines, live view (off makes a view read once) and archives (on by default: list rotated archives and search them too) — plus the running version. All toggles and settings persist in the browser (localStorage).

Find results show each file as an orange header with its match count, followed by the matching lines and their context. With the search box still empty, the headers alone are listed — the files that find is about to scan. Excerpts never repeat a line, and the ··· separator appears only between excerpts that really do have lines between them.

The file selector is a tree — and writable. Subfolders, and groups of files sharing a name prefix (per-host logs, say), are selectable entries — tail or find everything under one of them. Typing into the selector filters the entries, prefix-matching each path segment (ngi finds nginx/ anywhere in the tree); ArrowUp/Down and Enter select. Rotation leftovers (.gz, .bz2, .xz, .zst, numeric .1, date-stamped -YYYYMMDD, .old, .bak) never get appended, so they are never tailed, but the selector lists them and find searches them, decoded transparently — turn the ☰ menu's archives off to leave them out. In a multi-file stream, a file that has rotations carries a small gz button next to its name: one click selects that file together with all of its archives and switches to find. Binary files (a NUL byte in the first kilobyte — wtmp, journald files, stray executables) are not served at all.

Merged streams stay readable. In All files (the default) every line carries its file as a prefix in a stable per-file color, so interleaved sources are told apart at a glance — and clicking a prefix jumps into that file's view, scrolled to that very line. The merge recognizes the common timestamp formats (ISO 8601 / RFC 3339, YYYY-MM-DD HH:MM:SS, slash dates, Apache/CLF, Unix ctime, syslog) at or near the start of a line; a line without one keeps its file's previous timestamp, so multi-line entries stay together. Timestamps only order the merge — they are never added to the display.

Lines are interactive. Clicking selects a line, Ctrl+click toggles lines individually, Shift+click selects a range, Escape clears — and Ctrl-C copies exactly the highlighted lines, for pasting a few relevant entries into an issue or a chat. A normal mouse drag still selects and copies text natively.

Fast by design. On Linux, appended lines reach the browser in milliseconds via inotify (with a polling fallback elsewhere). Log files are append-only, and the frontend exploits that: every single-file view is cached with the byte offset read so far, so switching files or modes re-renders instantly and fetches only what arrived since — a fully-read archive is never requested again, and a rotated or truncated file resets its view. A view never ships more than the 50,000 lines the scrollback keeps, and big loads drive a real 0–100 progress bar under the toolbar.

Example: central syslog server

A common deployment is a host that aggregates logs from many machines via syslog-ng (or rsyslog) into a directory tree such as /var/log/remote. Point tailon-ng at the directory to serve everything under it recursively:

tailon-ng /var/log/remote/

Every file beneath it — including per-host subdirectories — shows up in the file picker, and All files streams them all merged in timestamp order.

Tailon-ng's server-side functionality is summarized entirely in its help message:

Usage: tailon-ng [options] <path> [<path> ...]

Tailon-ng is a webapp for searching through log files from your browser.

  -b, --bind string            Address and port to listen on (default ":8080")
  -h, --help                   Show this help message and exit
  -r, --relative-root string   Webapp relative root (default "/")

Tailon-ng is configured entirely through command-line flags.

Each <path> is a file, a directory, or a shell glob, where "*" matches within a
directory and "**" across them (so "/var/log/**.log" finds .log files at any
depth). Directories are served recursively, and new files are picked up as they
appear. Several paths can be given as separate arguments.

Rotation leftovers (.gz, .bz2, .xz, .zst, .1, -YYYYMMDD, .old, .bak) never get
appended, so they are never tailed — but the file selector lists them and find
searches them too, decompressed transparently. The web UI's "archives" switch
(in the hamburger menu, on by default) leaves them out of both.

On Linux, appended lines are pushed instantly via inotify; elsewhere, and on
filesystems without notification support, tailon-ng falls back to polling.

Example usage:
  tailon-ng /var/log/syslog /var/log/auth.log
  tailon-ng /var/log/nginx/ /var/log/apache/
  tailon-ng /var/log/remote/
  tailon-ng "/var/log/**.log"
  tailon-ng -b 127.0.0.1:8080 /var/log/messages

Security

No built-in authentication. By default tailon-ng is reachable by anyone who can connect to its address and port, and it serves — and lets clients download — exactly the files you point it at. Bind it to localhost (-b 127.0.0.1:8080) or put it behind an authenticating reverse proxy. It is a log viewer, not a gateway.

No shell, no external commands. Tailon-ng never runs tail, grep, sed, awk or any subprocess; reading, following and find are all done in-process in Go (search-as-you-type runs in the browser). The find query is a Go (RE2) regular expression, so nothing entered in the browser can cause shell or command injection.

Safe downloads. Files are served as plain-text attachments with X-Content-Type-Options: nosniff, so a log line that happens to look like HTML can't be rendered as script in your browser.

Development

Frontend

The frontend is plain, framework-free HTML, CSS and JavaScript: three flat files in frontend/ (one Go template main.html, main.css, main.js), embedded into the binary at compile time with //go:embed (see frontend.go). The favicon is an inline SVG data URI in main.html — no image files at all. There is no build step or toolchain — edit the files directly and rebuild the binary. The UI talks to the backend over Server-Sent Events.

Backend

The backend is written in straightforward Go, almost entirely standard library: flag parsing, configuration, HTTP serving, access logging, file following, server-side find and gzip/bzip2 decoding for the Server-Sent Events stream. The only third-party dependencies are ulikunitz/xz and klauspost/compress, which decode .xz and .zst archives. File reading and following live in tailer.go; the inotify wake-up path (Linux, raw syscall — no dependency) in watcher_linux.go, with the polling fallback in watcher_other.go.

Versioning

Verifiable release binaries are stamped with their git tag at build time (-ldflags "-X main.version=..." in the GitHub Actions workflow), which is what the UI's version badge shows. Local builds show dev; to stamp one yourself:

go build -ldflags "-X main.version=$(git describe --tags --always --dirty)"

Project lineage

Three distinct projects have carried the tailon name; this is the third, and the source of the recurring "wait, which tailon?" confusion:

  1. tailon-legacy — the first one, written in Python + Tornado with a custom TypeScript frontend.
  2. gvalkov/tailon — a full rewrite in Go with a Vue.js + SockJS frontend, configured through a file and released with GoReleaser. This is the upstream this repository is forked from.
  3. This fork (tailon-ng) — drops the third-party frontend and tooling for a framework-free, nearly dependency-free, single static binary. See How this fork differs from the original for the point-by-point comparison.

AI Usage

I view AI LLMs as a tool to help write faster and better code. AI assistants (Opus/Fable/Qwen/Gemma) wrote a good part of this tool. I reviewed and questioned the code piece by piece, the assistant explains, simplifies or fixes. The design calls and the final review stay with me, and every change is backed by tests (go test -race ./...). This tool is currently used in production.

Documentation

Overview

A webapp for looking at and searching through files.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL