rtail 0.2.1 → 2.0.0-rc.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +633 -82
  4. package/bin/rtail.js +97 -0
  5. package/package.json +15 -67
  6. package/.jscsrc +0 -28
  7. package/.jshintrc +0 -11
  8. package/.npmignore +0 -4
  9. package/.scss-lint.yml +0 -11
  10. package/cli/lib/webapp.js +0 -54
  11. package/cli/rtail-client.js +0 -156
  12. package/cli/rtail-server.js +0 -144
  13. package/dist/bundle.min.js +0 -18
  14. package/dist/css/bundle.min.css +0 -199
  15. package/dist/images/common/btn-fav-off.svg +0 -16
  16. package/dist/images/common/btn-fav-on.svg +0 -16
  17. package/dist/images/common/favicon-16x16.png +0 -0
  18. package/dist/images/common/favicon-32x32.png +0 -0
  19. package/dist/images/common/favicon-96x96.png +0 -0
  20. package/dist/images/common/icn-filter.svg +0 -16
  21. package/dist/images/common/icn-resume.svg +0 -16
  22. package/dist/images/common/logo-rtail.svg +0 -26
  23. package/dist/images/dark/btn-github.svg +0 -16
  24. package/dist/images/dark/btn-info.svg +0 -20
  25. package/dist/images/dark/btn-json-collapse.svg +0 -16
  26. package/dist/images/dark/btn-json-expand.svg +0 -16
  27. package/dist/images/dark/btn-settings.svg +0 -16
  28. package/dist/images/dark/icn-close-date.svg +0 -16
  29. package/dist/images/dark/icn-font-decrease.svg +0 -19
  30. package/dist/images/dark/icn-font-increase.svg +0 -19
  31. package/dist/images/dark/icn-font-reset.svg +0 -18
  32. package/dist/images/dark/icn-search.svg +0 -16
  33. package/dist/images/dark/icn-sort-asc.svg +0 -18
  34. package/dist/images/dark/icn-sort-desc.svg +0 -18
  35. package/dist/images/dark/icn-theme-dark.svg +0 -18
  36. package/dist/images/dark/icn-theme-light.svg +0 -26
  37. package/dist/images/dark/logo-lukibear.svg +0 -74
  38. package/dist/images/dark/logo-rtail-info.svg +0 -26
  39. package/dist/images/light/btn-github.svg +0 -16
  40. package/dist/images/light/btn-info.svg +0 -20
  41. package/dist/images/light/btn-json-collapse.svg +0 -16
  42. package/dist/images/light/btn-json-expand.svg +0 -16
  43. package/dist/images/light/btn-settings.svg +0 -16
  44. package/dist/images/light/icn-close-date.svg +0 -16
  45. package/dist/images/light/icn-font-decrease.svg +0 -19
  46. package/dist/images/light/icn-font-increase.svg +0 -19
  47. package/dist/images/light/icn-font-reset.svg +0 -18
  48. package/dist/images/light/icn-search.svg +0 -16
  49. package/dist/images/light/icn-sort-asc.svg +0 -18
  50. package/dist/images/light/icn-sort-desc.svg +0 -18
  51. package/dist/images/light/icn-theme-dark.svg +0 -18
  52. package/dist/images/light/icn-theme-light.svg +0 -26
  53. package/dist/images/light/logo-lukibear.svg +0 -74
  54. package/dist/images/light/logo-rtail-info.svg +0 -26
  55. package/dist/index.html +0 -1
  56. package/scripts/npm-publish.sh +0 -21
  57. package/scripts/set-version.sh +0 -7
  58. package/test/rtail-client.test.js +0 -131
  59. package/test/rtail-server.test.js +0 -138
  60. package/test/runner.js +0 -31
  61. package/test/util.js +0 -65
package/README.md CHANGED
@@ -1,26 +1,256 @@
1
1
  # `rtail(1)`
2
2
 
3
- [![Wercker CI](https://img.shields.io/wercker/ci/556547b7be632a8c751c857d.svg?style=flat-square)](https://app.wercker.com/project/bykey/54b073dac5b9156509c26031c78c98d4)
4
- [![Coveralls](https://img.shields.io/coveralls/kilianc/rtail.svg?style=flat-square)](https://coveralls.io/r/kilianc/rtail)
3
+ [![CI](https://github.com/kilianc/rtail/actions/workflows/ci.yml/badge.svg)](https://github.com/kilianc/rtail/actions/workflows/ci.yml)
5
4
  [![NPM version](https://img.shields.io/npm/v/rtail.svg?style=flat-square)](https://www.npmjs.com/package/rtail)
6
5
  [![NPM downloads](https://img.shields.io/npm/dm/rtail.svg?style=flat-square)](https://www.npmjs.com/package/rtail)
7
6
  [![GitHub Stars](https://img.shields.io/github/stars/kilianc/rtail.svg?style=flat-square)](https://github.com/kilianc/rtail)
8
- [![License](https://img.shields.io/npm/l/rtail.svg?style=flat-square)](https://www.npmjs.com/package/rtail)
7
+ [![License](https://img.shields.io/badge/license-Apache--2.0-04cd7e.svg?style=flat-square)](LICENSE)
9
8
  [![Gitter](https://img.shields.io/badge/≡_gitter-join_chat_➝-04cd7e.svg?style=flat-square)](https://gitter.im/kilianc/rtail?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
10
9
 
11
10
  ## Terminal output to the browser in seconds, using UNIX pipes.
12
11
 
12
+ <picture>
13
+ <source media="(prefers-color-scheme: dark)" srcset="docs/media/explorer-dark.png">
14
+ <img alt="The rtail explorer: level>=WARN on the api stream over the last hour, a wall of upstream timeouts from payments at the top" src="docs/media/explorer-light.png">
15
+ </picture>
16
+
17
+ *`level>=WARN` on the `api` stream over the last hour: a run of upstream
18
+ timeouts from payments in the last few minutes, with `service`, `status`,
19
+ `latency_ms` and `region` promoted to columns.*
20
+
21
+ > **v2 — release candidate.** v2 is a rewrite: a durable, searchable log store
22
+ > backed by Parquet and SQL. The server is a single Go binary with the webapp
23
+ > embedded, and the npm package carries only the client pipe.
24
+ >
25
+ > The `cmd | rtail` wire format is frozen and always will be — the client you
26
+ > already have installed works against a v2 server unchanged.
27
+ >
28
+ > **Where it is:** everything described below is built and tested — storage,
29
+ > search, compaction, the explorer, the four ingest protocols, authentication,
30
+ > TLS, and a release pipeline that publishes four tarballs, six npm packages
31
+ > and a container image per tag. `2.0.0-rc.3` is the current release; until
32
+ > `2.0.0` is tagged, reaching it takes `RTAIL_PRERELEASE=1` — see below.
33
+
13
34
  `rtail` is a command line utility that grabs every line in `stdin` and broadcasts it over **UDP**. That's it. Nothing fancy. Nothing complicated. Tail log files, app output, or whatever you wish, using `rtail` broadcasting to an `rtail-server` – See multiple streams in the browser, in realtime.
14
35
 
15
- ## Installation
36
+ ## Running the server
37
+
38
+ The server ships as a container image — this is the recommended way to run it.
39
+
40
+ $ docker run -d --name rtail -p 8888:8888 -p 9999:9999/udp ghcr.io/kilianc/rtail:next
41
+
42
+ `:next` is the newest release candidate, the way `rtail@next` is on npm.
43
+ `latest` only moves on a stable release, so until `2.0.0` is tagged it is
44
+ stuck on an earlier candidate that predates `/data`; once it is, the plain
45
+ `ghcr.io/kilianc/rtail` is right again.
46
+
47
+ Open <http://localhost:8888> and start piping. There is a
48
+ [`docker-compose.yml`](docker-compose.yml) if you prefer:
49
+
50
+ $ docker compose up -d
51
+
52
+ Options are flags, or `RTAIL_*` environment variables:
53
+
54
+ $ docker run -d -p 8080:8080 -p 9999:9999/udp \
55
+ -e RTAIL_WEB_PORT=8080 ghcr.io/kilianc/rtail:next
56
+
57
+ $ docker run -d -p 8888:8888 -p 9999:9999/udp \
58
+ ghcr.io/kilianc/rtail:next --backlog 500
59
+
60
+ The image is memory-only until you give it somewhere to write. `/data` is
61
+ there for that, owned by the user the server runs as:
62
+
63
+ $ docker run -d -p 8888:8888 -p 9999:9999/udp \
64
+ -e RTAIL_DATA=/data -v rtail-data:/data ghcr.io/kilianc/rtail:next
65
+
66
+ A named volume, as above, takes that owner on its own. A bind mount keeps the
67
+ host directory's, which the server's uid 10001 cannot write to:
68
+ `chown 10001:10001` the directory first, or run it as yourself with
69
+ `--user "$(id -u):$(id -g)"`.
70
+
71
+ The image binds `0.0.0.0` inside the container, so **the published ports are
72
+ reachable from your whole network**. It is open by default;
73
+ `-e RTAIL_AUTH_TOKEN="$(openssl rand -hex 32)"` closes it — see
74
+ [Authentication](#authentication). Without a token, keep it on a trusted
75
+ network or put a reverse proxy in front of it.
76
+
77
+ ## Releases
78
+
79
+ Tagged releases carry a binary for each platform and a container image.
80
+
81
+ $ docker run -p 8888:8888 -p 9999:9999/udp ghcr.io/kilianc/rtail:next
82
+
83
+ Or take the binary for your platform from the
84
+ [releases page](https://github.com/kilianc/rtail/releases) — `linux_amd64`,
85
+ `linux_arm64`, `darwin_amd64`, `darwin_arm64`, with a `checksums.txt` beside
86
+ them. There is no Windows build: DuckDB is cgo and nobody has asked.
87
+
88
+ The binary is not static, because DuckDB is cgo. Linux builds are linked in
89
+ Debian bookworm, so they need glibc 2.36 or newer; the container image is the
90
+ answer on anything older. Every binary is started and asked for `/healthz`
91
+ before it is published, so a release that cannot run does not become one.
92
+
93
+ ## Installing the client
94
+
95
+ The client is a UNIX pipe and belongs on the host whose output you are tailing,
96
+ not in the container. It is a single Go binary, about 2.6 MB, with no runtime
97
+ of any kind:
98
+
99
+ # the short way
100
+ $ curl -fsSL https://raw.githubusercontent.com/kilianc/rtail/main/install.sh | sh
101
+
102
+ # or take the tarball yourself — it is what the line above downloads
103
+ $ tar -xzf rtail_2.0.0-rc.3_darwin_arm64.tar.gz
104
+ $ sudo mv rtail_2.0.0-rc.3_darwin_arm64/rtail /usr/local/bin
105
+
106
+ # or, if you have Go
107
+ $ go install github.com/kilianc/rtail/v2/cmd/rtail@latest
108
+
109
+ # or, if you are already standing in a project
110
+ $ npm install -g rtail@next
111
+
112
+ **While 2.0 is a release candidate, two of those four need telling.** GitHub's
113
+ "latest release" skips prereleases, so the plain `install.sh` stops and asks
114
+ for `RTAIL_PRERELEASE=1`; and npm's `latest` tag still points at `0.2.1` — the
115
+ v1 Node client — so a plain `npm install -g rtail` quietly hands you that:
116
+
117
+ $ curl -fsSL https://raw.githubusercontent.com/kilianc/rtail/main/install.sh | RTAIL_PRERELEASE=1 sh
118
+ $ npm install -g rtail@next
119
+
120
+ `go install …@latest` needs nothing: there is no `v2` release tag yet, so Go
121
+ resolves it to the newest prerelease. Taking the tarball by name needs nothing
122
+ either. When `2.0.0` is tagged, all four plain forms are correct.
123
+
124
+ [`install.sh`](install.sh) detects your platform, checks the download against
125
+ the release's own `checksums.txt` and refuses to continue on a mismatch,
126
+ installs one binary into `/usr/local/bin` — or `~/.local/bin` if that is not
127
+ writable, because a script piped from the internet should not be the thing
128
+ that asks for your password — and touches nothing else. It never calls `sudo`
129
+ and never edits your shell profile. `RTAIL_VERSION`, `RTAIL_INSTALL_DIR` and
130
+ `RTAIL_SERVER=1` override the obvious things. It is a hundred lines of POSIX
131
+ sh; `curl -fsSL https://raw.githubusercontent.com/kilianc/rtail/main/install.sh | less` reads the whole of it,
132
+ and doing that before you pipe anything into `sh` is a good habit generally.
16
133
 
17
- $ npm install -g rtail
134
+ The npm route is a convenience, not a dependency: the package holds a short
135
+ launcher and one optional dependency per platform, each carrying a prebuilt
136
+ binary and declaring the `os` and `cpu` it is for. npm downloads the one that
137
+ matches and skips the rest. There is no postinstall script and nothing is
138
+ fetched at install time, so it works behind a proxy and under
139
+ `--ignore-scripts`. The launcher needs Node 20 or newer; the other two routes
140
+ need nothing.
18
141
 
19
- ## Web app
142
+ Windows is published to npm and not as a tarball — the client cross-compiles
143
+ anywhere, the server does not.
20
144
 
21
- ![](https://s3.amazonaws.com/rtail/github/dark.png)
145
+ ## The explorer
22
146
 
23
- ![](https://s3.amazonaws.com/rtail/github/light.png)
147
+ **One filter drives everything.** Type it and the live tail narrows — the
148
+ server compiles the same rQL to a predicate over the ingest stream. Switch to
149
+ history and the same query runs against Parquet. Same semantics either way,
150
+ because there is one parser behind both.
151
+
152
+ **The histogram is the navigation.** Counts over time stacked by severity; drag
153
+ across it to zoom the range.
154
+
155
+ **Most queries are built by clicking.** Expand any event and every value
156
+ carries "filter to this" and "exclude this"; the command bar updates to match,
157
+ so whatever you built by pointing is something you could have typed. The field
158
+ sidebar shows the top values of every field *within the current results*, which
159
+ answers "what is even in here" before you know what to search for.
160
+
161
+ **Completion is real.** It comes from the catalog's key inventory — every field
162
+ that has actually appeared, with its type and how often — not from a guess at
163
+ what a log line might contain.
164
+
165
+ **Every view is a link.** Stream, filter, time range, promoted columns, and
166
+ whether the timeline is open — all of it lives in the URL, so the screenshot
167
+ above is a URL somebody could send you.
168
+
169
+ **Any field can become a column.** The ⊞ beside a field in the sidebar pulls it
170
+ out of the summary into a column of its own, and the × on the header puts it
171
+ back.
172
+
173
+ **The query pane holds everything that acts on the query** — the language
174
+ switch, the examples, the time range and Run — inside its own border, because
175
+ they belong to the query rather than to the page.
176
+
177
+ Keyboard: `/` or `⌘K` focuses the editor, `⌘⏎` runs it (plain `⏎` is a
178
+ newline — it is an editor), `j`/`k` move between events, `⎋` collapses
179
+ everything.
180
+
181
+ ## Getting logs in
182
+
183
+ Four ways, and every one of them lands in the same envelope — a record is
184
+ indistinguishable once stored, whichever door it came through.
185
+
186
+ **UNIX pipes**, the original, frozen forever:
187
+
188
+ $ node server.js 2>&1 | rtail --id api.example.com
189
+
190
+ **HTTP**, for anything that would mind losing a line. UDP is at-most-once and
191
+ capped at one datagram; this acknowledges. With `--data`, a 200 means every
192
+ line has been fsynced to the write-ahead log. Without it the server keeps only
193
+ its in-memory ring, and a 200 means the lines were received, not stored:
194
+
195
+ $ curl -XPOST 'localhost:8888/v1/ingest?stream=api' --data-binary @- <<'JSON'
196
+ {"level":"error","msg":"upstream timeout","service":"api","latency_ms":30004}
197
+ a plain line works too
198
+ JSON
199
+
200
+ Anything else may arrive after part of the batch was already stored — a store
201
+ that fails halfway through is a 503, a body past `--max-body-mb` a 413 — so
202
+ every response says how many lines it `accepted`, and those first lines are
203
+ durable. Resume after them rather than replaying the batch.
204
+
205
+ Newline-delimited JSON or plain text — the content type is not consulted,
206
+ because the normalizer already decides per line what it is looking at. `gzip`
207
+ and `zstd` bodies are decoded.
208
+
209
+ **OpenTelemetry**, protobuf or JSON, at `/v1/logs`. Point any collector at it
210
+ with a config line and no code change:
211
+
212
+ exporters:
213
+ otlphttp:
214
+ logs_endpoint: http://rtail:8888/v1/logs
215
+
216
+ `service.name` becomes the stream, log and resource attributes become fields,
217
+ and `trace_id`/`span_id` are promoted so correlation actually works. A 200
218
+ here promises what it does on `/v1/ingest`.
219
+
220
+ **Syslog**, RFC5424 and RFC3164, UDP and TCP on the same port:
221
+
222
+ $ rtail-server --data ./logs --syslog-host 0.0.0.0 --syslog-port 5140
223
+
224
+ # rsyslog.conf
225
+ *.* @@rtail-host:5140;RSYSLOG_SyslogProtocol23Format
226
+
227
+ `--syslog-host` defaults to `--udp-host` — loopback, unless you moved it — so
228
+ a remote rsyslog needs the `0.0.0.0`. Syslog carries no token: `--auth-token`
229
+ covers HTTP only, and nothing refuses this bind without one, so off loopback
230
+ you are trusting your network.
231
+
232
+ APP-NAME becomes the stream, the priority becomes a real severity — so
233
+ `level>=ERROR` works on syslog without anyone writing a parser — and RFC5424
234
+ structured data is promoted to fields.
235
+
236
+ Anything that can write NDJSON to an HTTP endpoint works without a plugin:
237
+
238
+ # vector.toml
239
+ [sinks.rtail]
240
+ type = "http"
241
+ inputs = ["my_source"]
242
+ uri = "http://rtail:8888/v1/ingest?stream=vector"
243
+ encoding.codec = "json"
244
+ framing.method = "newline_delimited"
245
+
246
+ # fluent-bit.conf
247
+ [OUTPUT]
248
+ Name http
249
+ Match *
250
+ Host rtail
251
+ Port 8888
252
+ URI /v1/ingest?stream=fluentbit
253
+ Format json_lines
24
254
 
25
255
  ## Rationale
26
256
 
@@ -33,9 +263,130 @@ There are many log aggregation tools out there, but few of them are realtime. **
33
263
  **The `rtail` approach is very simple:**
34
264
  * pipe something into `rtail` using [UNIX I/O redirection](http://www.westwind.com/reference/os-x/commandline/pipes.html) [[2]](http://www.codecoffee.com/tipsforlinux/articles2/042.html)
35
265
  * broadcast every line using UDP
36
- * `rtail-server`, **if listening**, will dispatch the stream into your browser, using [socket.io](http://socket.io/).
266
+ * `rtail-server`, **if listening**, will dispatch the stream into your browser, using [server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events).
267
+
268
+ `rtail` is a realtime debugging and monitoring tool, which can display multiple aggregate streams via a modern web interface. **With `--data` it now keeps them too, and lets you search them with SQL** — see [Storage](#storage) and [Search](#search) below.
269
+
270
+ ## Storage
271
+
272
+ $ rtail-server --data ./logs
273
+
274
+ Point `--data` at a directory and every line is written to Parquet. Without it,
275
+ rtail is what it always was: an in-memory ring, nothing on disk, nothing to
276
+ clean up.
277
+
278
+ **There is no schema to define.** The envelope — timestamp, stream, level,
279
+ message, host, sequence, and the original line — is always there. Everything
280
+ else is whatever your logs happened to contain: each root-level JSON key
281
+ becomes its own typed column, and nested objects are kept as JSON you can
282
+ path into. Two files written a week apart with completely different keys
283
+ reconcile by name at read time.
284
+
285
+ **Your logs are just files.** The layout is plain and browsable:
286
+
287
+ logs/
288
+ catalog.sqlite
289
+ wal/
290
+ streams/api.example.com/2026/08/11/L0-…parquet
291
+
292
+ which means any tool reads them, with rtail uninstalled and nothing exported:
293
+
294
+ $ duckdb -c "
295
+ SELECT a_service, count(*)
296
+ FROM read_parquet('logs/streams/**/*.parquet', union_by_name := true)
297
+ WHERE level = 'ERROR' AND a_latency_ms > 500
298
+ GROUP BY 1"
37
299
 
38
- `rtail` is a realtime debugging and monitoring tool, which can display multiple aggregate streams via a modern web interface. **There is no persistent layer, nor does the tool store any data**. If you need a persistent layer, use something like [loggly](https://www.loggly.com/).
300
+ That is the point, and it is covered by a test rather than merely claimed.
301
+
302
+ **Durability.** Lines land in a write-ahead log before anything else, and are
303
+ batched into Parquet from there. The WAL is fsynced every 100ms, so an
304
+ unclean shutdown costs at most that; a clean one costs nothing. Recovery
305
+ replays whatever the previous process did not finish, and is idempotent —
306
+ crashing midway through a flush and restarting produces the same files, not
307
+ duplicates.
308
+
309
+ **Compaction.** A flush every thirty seconds per stream is thousands of tiny
310
+ files a day, each with a footer to open. A background pass merges them:
311
+
312
+ | level | holds | sorted by |
313
+ |---|---|---|
314
+ | L0 | one flush | `seq` — the order it arrived in |
315
+ | L1 | an hour | `ts` |
316
+ | L2 | a day | `(level, ts)`, or `--cluster-by` |
317
+
318
+ Sort order is the point, not the file count. Sorting by time makes range
319
+ pruning exact at row-group granularity rather than merely likely, and a
320
+ low-cardinality leading column turns `level>=ERROR` over a day into a
321
+ row-group skip instead of a scan. Compaction is also where a messy population
322
+ of schemas becomes one clean union — types that disagreed get widened,
323
+ always-null columns disappear.
324
+
325
+ In practice six flushes of a small stream merge from 24KB to 5KB.
326
+
327
+ A merge either publishes its output and retires its inputs, or changes
328
+ nothing. Retired files stay on disk for a grace period, so a query that
329
+ started before the merge still has its files. `--compact-interval 0` turns the
330
+ whole thing off.
331
+
332
+ **Retention** is per-server, and all of it happens during compaction:
333
+
334
+ $ rtail-server --data ./logs \
335
+ --retention 30d \
336
+ --retention-raw 7d \
337
+ --downsample-after 14d --downsample-level WARN
338
+
339
+ `--retention` deletes whole files once their newest record is past the
340
+ horizon; a file straddling the boundary survives until all of it has expired,
341
+ because rewriting a file to drop a few rows costs far more than keeping them.
342
+ `--retention-raw` drops the original line while keeping every promoted column,
343
+ which roughly halves the footprint — the cost is that keys which were never
344
+ promoted stop being queryable, since `raw` was their fallback.
345
+ `--downsample-after` discards anything below `--downsample-level` once it is
346
+ old enough.
347
+
348
+ Durations take `d` and `w` as well as Go's own units, because `--retention
349
+ 720h` is nobody's idea of clear.
350
+
351
+ ## Search
352
+
353
+ Two ways in, sharing one set of semantics.
354
+
355
+ **The filter bar** — terse, and what most searching looks like:
356
+
357
+ level>=ERROR service=api "connection timeout"
358
+ -path:/health AND (region=us-east-1 OR region=us-west-2)
359
+ latency_ms>500 req.path:~"^/v1/"
360
+ trace_id=*
361
+
362
+ `k=v` is equality, `k:v` a case-insensitive substring, `k:~re` a regular
363
+ expression, `k>n` a comparison, `k=*` "the key exists", `k=null` "it does not".
364
+ A bare word is full-text across the message and the original line. Terms are
365
+ ANDed unless you write `OR`; `-` and `NOT` negate; parentheses group.
366
+
367
+ `level` compares by **severity, not alphabetically** — `level>=ERROR` matches
368
+ FATAL too, which is what you meant. Quoting a value suppresses type coercion,
369
+ so `status=200` is the number and `status="200"` is the string.
370
+
371
+ **Raw SQL**, for everything else:
372
+
373
+ $ curl -XPOST localhost:8888/v1/sql --data-binary @- <<'JSON'
374
+ {
375
+ "sql": "SELECT a_service, count(*) n, quantile_cont(a_latency_ms, 0.95) p95 FROM logs WHERE level = 'ERROR' GROUP BY 1 ORDER BY n DESC",
376
+ "from": "-6h"
377
+ }
378
+ JSON
379
+
380
+ The query is one line because JSON strings cannot hold a raw newline; write
381
+ `\n` if you want the SQL to keep its shape.
382
+
383
+ `logs` is a view over the pruned files. The query runs read-only in a DuckDB
384
+ sandbox that can see nothing but your data directory.
385
+
386
+ **The time range is the partition pruner**, not decoration. It is always
387
+ applied — a query without one gets the last 24 hours injected — and every
388
+ response reports the window it used along with how many files and rows the
389
+ query implied reading, so a slow search can explain itself.
39
390
 
40
391
  ## Examples
41
392
 
@@ -70,11 +421,10 @@ For fun and debugging:
70
421
 
71
422
  Options:
72
423
  --host, -h The server host [string] [default: "127.0.0.1"]
73
- --port, -p The server port [string] [default: 9999]
424
+ --port, -p The server port [number] [default: 9999]
74
425
  --id, --name The log stream id [string] [default: (moniker)]
75
426
  --mute, -m Don't pipe stdin with stdout [boolean]
76
427
  --tty Keeps ansi colors [boolean] [default: true]
77
- --parse-date Looks for dates to use as timestamp [boolean] [default: true]
78
428
  --help Show help [boolean]
79
429
  --version, -v Show version number [boolean]
80
430
 
@@ -85,14 +435,14 @@ For fun and debugging:
85
435
  server | rtail --port 43567 Uses custom port
86
436
  server | rtail --mute No stdout
87
437
  server | rtail --no-tty Strips ansi colors
88
- server | rtail --no-date-parse Disable date parsing/stripping
89
-
90
438
 
91
439
  ## `rtail-server(1)`
92
440
 
93
- `rtail-server` receives all messages broadcast from every `rtail` client, displaying all incoming log streams in a realtime web view. **Under the hood, the server uses [socket.io](http://socket.io) to pipe every incoming UDP message to the browser.**
441
+ `rtail-server` receives all messages broadcast from every `rtail` client, displaying all incoming log streams in a realtime web view. **Under the hood it is a single Go binary with the webapp embedded, pushing every incoming UDP message to the browser over server-sent events.**
94
442
 
95
- There is little to no configuration – The default UDP/HTTP ports can be changed, but that's it.
443
+ Everything is a flag, or the same name as an `RTAIL_*` environment variable.
444
+ The defaults are chosen so that `rtail-server` with no arguments is the v1
445
+ behaviour: both listeners on loopback, an in-memory ring, nothing on disk.
96
446
 
97
447
  ## Examples
98
448
 
@@ -100,61 +450,258 @@ Use default values:
100
450
 
101
451
  $ rtail-server
102
452
 
103
- Always use latest, stable webapp:
104
-
105
- $ rtail-server --web-version stable
106
-
107
453
  Use custom ports:
108
454
 
109
455
  $ rtail-server --web-port 8080 --udp-port 9090
110
456
 
111
457
  Set debugging on:
112
458
 
113
- $ DEBUG=rtail:* rtail-server
459
+ $ rtail-server --verbose
114
460
 
115
461
  Open your browser and start tailing logs!
116
462
 
117
463
  ## Params
118
464
 
119
- $ rtail-server --help
120
- Usage: rtail-server [OPTIONS]
121
-
122
- Options:
123
- --udp-host, --uh The listening UDP hostname [default: "127.0.0.1"]
124
- --udp-port, --up The listening UDP port [default: 9999]
125
- --web-host, --wh The listening HTTP hostname [default: "127.0.0.1"]
126
- --web-port, --wp The listening HTTP port [default: 8888]
127
- --web-version Define web app version to serve [string]
128
- --help, -h Show help [boolean]
129
- --version, -v Show version number [boolean]
130
-
131
- Examples:
132
- rtail-server --web-port 8080 Use custom HTTP port
133
- rtail-server --udp-port 8080 Use custom UDP port
134
- rtail-server --web-version stable Always uses latest stable webapp
135
- rtail-server --web-version unstable Always uses latest develop webapp
136
- rtail-server --web-version 0.1.3 Use webapp v0.1.3
465
+ `rtail-server --help` prints the lot. Grouped by what they touch:
466
+
467
+ **Listening**
468
+
469
+ | flag | default | |
470
+ |---|---|---|
471
+ | `--web-host` | `127.0.0.1` | HTTP listen address |
472
+ | `--web-port` | `8888` | HTTP port |
473
+ | `--udp-host` | `127.0.0.1` | UDP listen address |
474
+ | `--udp-port` | `9999` | UDP port |
475
+ | `--syslog-host` | `--udp-host` | syslog listen address |
476
+ | `--syslog-port` | `0` | syslog on this port, UDP and TCP; `0` disables it |
477
+
478
+ **Storage** — see [Storage](#storage)
479
+
480
+ | flag | default | |
481
+ |---|---|---|
482
+ | `--data` | unset | directory for durable storage; unset is memory only |
483
+ | `--backlog` | `100` | lines of live history kept per stream |
484
+ | `--compact-interval` | `5m` | how often to compact; `0` disables it |
485
+ | `--cluster-by` | `level` | leading sort column for daily files; empty is time only |
486
+ | `--retention` | `0` | delete data older than this; `0` keeps it forever |
487
+ | `--retention-raw` | `0` | drop the original line after this, keeping the columns |
488
+ | `--downsample-after` | `0` | discard low-severity records older than this |
489
+ | `--downsample-level` | `WARN` | the lowest severity `--downsample-after` keeps |
490
+
491
+ **Ingest**
492
+
493
+ | flag | default | |
494
+ |---|---|---|
495
+ | `--ingest-stream` | `http` | stream for HTTP records that name none |
496
+ | `--max-body-mb` | `32` | largest accepted ingest request |
497
+
498
+ **Access** — see [Authentication](#authentication)
499
+
500
+ | flag | default | |
501
+ |---|---|---|
502
+ | `--auth-token` | unset | require this bearer token on the HTTP API and webapp |
503
+ | `--insecure` | off | allow a non-loopback address with no `--auth-token` |
504
+ | `--tls-cert`, `--tls-key` | unset | serve HTTPS; both or neither |
505
+
506
+ **Everything else**
507
+
508
+ | flag | default | |
509
+ |---|---|---|
510
+ | `--web-root` | unset | serve the webapp from this directory instead of the binary |
511
+ | `--verbose` | off | log at debug level |
512
+ | `--version` | | print the version and exit |
513
+
514
+ Each one is also an environment variable, upper-cased and prefixed with
515
+ `RTAIL_` — `RTAIL_WEB_PORT=8080`, `RTAIL_BACKLOG=500`,
516
+ `RTAIL_DATA=/data`. That is how the container image is configured.
517
+ The v1 short aliases still work too: `--uh`, `--up`, `--wh`, `--wp`, `-b`.
518
+
519
+ Note that `--backlog` is about the *live view* — how many recent lines a
520
+ freshly opened browser tab is handed. How much is kept on disk is
521
+ `--retention`'s business, and is unlimited until you set it.
522
+
523
+ ## API
524
+
525
+ The webapp is a client of a small documented API, so anything else can be too:
526
+
527
+ | endpoint | what it does |
528
+ |---|---|
529
+ | `GET /v1/streams` | every known stream |
530
+ | `GET /v1/tail?stream=<name>` | SSE feed: `streams`, `backlog`, then `line` events. Omit `stream` to tail every stream at once, backlog merged |
531
+ | `GET /v1/search` | matching records, keyset-paginated |
532
+ | `GET /v1/histogram` | match counts over time, split by severity |
533
+ | `GET /v1/fields?field=<name>` | the most common values of a field, within the current filter |
534
+ | `GET /v1/schema` | every key ever seen, with its type — what autocomplete completes against |
535
+ | `POST /v1/sql` | read-only SQL over a `logs` view |
536
+ | `POST /v1/ingest` | NDJSON or plain lines, optionally gzip/zstd |
537
+ | `POST /v1/logs` | OTLP logs, protobuf or JSON |
538
+ | `GET /healthz` | version, stream count, store size, per-receiver ingest counters |
539
+
540
+ The search endpoints share their parameters: `q` (filter), `stream`, `from` and
541
+ `to` (RFC3339, epoch millis, or a relative `-6h` or `-7d`), `limit`, `cursor`, `order`.
542
+ They return 503 with an explanation when the server is running without
543
+ `--data`, since there is nothing to search.
137
544
 
138
545
  ## UDP Broadcasting
139
546
 
140
547
  To scale and broadcast on multiple servers, instruct the `rtail` client to stream to the broadcast address. Every message will then be delivered to all servers in your subnet.
141
548
 
142
- ## Authentication layer
549
+ ## Authentication
550
+
551
+ One shared token, or nothing.
143
552
 
144
- For the time being, the webapp doesn't have an authentication layer; it assumes that you will run it behind a VPN or reverse proxy, with a simple `Authorization` header check.
553
+ $ rtail-server --auth-token "$(openssl rand -hex 32)" --web-host 0.0.0.0
554
+
555
+ With a token set, every route except `/healthz` requires it. Programs send it
556
+ as a header; browsers sign in once and get a cookie.
557
+
558
+ $ curl -H "Authorization: Bearer $TOKEN" http://logs.example.com/v1/streams
559
+
560
+ `X-Rtail-Token` works too, for clients that reserve `Authorization`. In a
561
+ browser, opening the server without one gives you a sign-in form, and
562
+ `https://logs.example.com/?token=…` signs you in from a link. It is the shared
563
+ token itself, not a one-time code, so treat the link as the secret it is: the
564
+ server sets the cookie and redirects the token out of the address bar so it
565
+ does not end up in your history or a bookmark.
566
+
567
+ `/healthz` is deliberately left open, so a container healthcheck or a load
568
+ balancer keeps working without being handed the secret. It reports liveness,
569
+ the version and a handful of numbers — how many streams, how much is stored,
570
+ what each receiver has counted — and never a name; everything that could show
571
+ a log line is behind the token.
572
+
573
+ **Running with no token is refused on any address but loopback.** This is the
574
+ one rule the server enforces on your behalf:
575
+
576
+ $ rtail-server --web-host 0.0.0.0
577
+ rtail-server: refusing to listen on 0.0.0.0 with no --auth-token:
578
+ anyone who can reach it could read every log.
579
+
580
+ Pass `--insecure` if something in front of it handles access. Nothing stops
581
+ you; it just will not happen by accident.
582
+
583
+ What this deliberately is not: accounts, roles, OIDC, SAML, or per-stream
584
+ permissions. rtail is a single-node tool, and anyone who needs those already
585
+ has a reverse proxy that does them properly.
586
+
587
+ **Two holes worth knowing about.** UDP ingest is unauthenticated, because UDP
588
+ has no handshake to carry a token and v1's wire format is frozen. Syslog is
589
+ unauthenticated for the same reason. Both listen on `127.0.0.1` by default; if
590
+ you move either off loopback you are trusting your network.
591
+
592
+ ### TLS
593
+
594
+ $ rtail-server --tls-cert cert.pem --tls-key key.pem
595
+
596
+ Both or neither — half a pair is refused at startup rather than failing on
597
+ somebody's first request. There is no ACME client and there will not be one:
598
+ a certificate is the one thing every reverse proxy already solves.
599
+
600
+ # Running it locally
601
+
602
+ Note there are two images, and they are not the same thing:
603
+ [`Dockerfile`](Dockerfile) is the server you deploy, and
604
+ [`tools/Dockerfile`](tools/Dockerfile) is the development toolchain, which
605
+ mounts your checkout.
606
+
607
+ The toolchain lives in a container ([`tools/Dockerfile`](tools/Dockerfile)), so Docker is
608
+ the only thing you need installed — no Node.js, no Go, no npm, no global CLIs.
609
+ v2 needs both toolchains: Node builds the webapp, Go builds the server that
610
+ embeds it, and the image carries pinned versions of each.
611
+
612
+ $ make dev
613
+
614
+ That builds the assets and the server binary, starts `rtail-server`, feeds it
615
+ three live demo streams, and serves the webapp. It prints the URL — `make url`
616
+ prints it again. Stylesheets and the bundle recompile on save; reload the
617
+ browser to pick them up. Go changes need a `make dev` restart, because the
618
+ binary is rebuilt at startup. `Ctrl-C` stops everything.
619
+
620
+ The first run also builds the toolchain image and installs dependencies, so it
621
+ takes a minute; subsequent runs start immediately.
622
+
623
+ To leave it running in the background instead:
624
+
625
+ $ make up # start detached, wait for it, print the URL
626
+ $ make logs # tail it
627
+ $ make down # stop it
628
+
629
+ Other targets:
630
+
631
+ $ make server # build bin/rtail-server
632
+ $ make client # build bin/rtail
633
+ $ make release # build the self-contained server, webapp embedded
634
+ $ make build # build the webapp into app/ (development)
635
+ $ make dist # build the minified webapp into web/dist
636
+ $ make ci # everything CI runs, before you spend a runner minute
637
+ $ make test # go test -race ./...
638
+ $ make vet # go vet
639
+ $ make fmt # gofmt the Go sources
640
+ $ make typecheck # type-check the webapp
641
+ $ make npm-pack # lay out the npm packages under dist/npm
642
+ $ make screenshots # retake the screenshot at the top of this README
643
+ $ make shell # open a shell inside the toolchain container
644
+ $ make clean # remove generated assets, binaries and dependencies
645
+
646
+ ### Ports
647
+
648
+ Ports are derived from the worktree path, so several checkouts of this repo can
649
+ run at once without colliding. Each gets its own HTTP port, UDP port, and
650
+ container name, fixed across runs — sharing any of the three fails confusingly,
651
+ since two servers can both bind the same UDP port and the loser simply never
652
+ receives a log line.
653
+
654
+ `make url` reports the current one. Override with `make dev PORT=9000
655
+ UDP_PORT=9001`.
656
+
657
+ If you have Node on your machine, the webapp targets are plain npm scripts —
658
+ `npm install`, then `npm run build`, `npm run dist`, `npm run typecheck`.
659
+ `npm run dev` also spawns `bin/rtail-server` and `bin/rtail`, so it needs Go
660
+ as well, to build those two for your machine. `make server client` is not a
661
+ substitute on a Mac: both build inside the Linux toolchain container, so what
662
+ lands in `bin/` is a Linux binary — right for `make dev`, which runs there, and
663
+ not something macOS will start. Node builds the webapp and is not in the
664
+ product: the server embeds the bundle, and the client is a Go binary.
665
+
666
+ ### The stack
667
+
668
+ | | |
669
+ | --- | --- |
670
+ | Client | Go, one static binary, [json5](https://github.com/titanous/json5) its only dependency |
671
+ | Server | Go + [DuckDB](https://duckdb.org) + [parquet-go](https://github.com/parquet-go/parquet-go) |
672
+ | Webapp | [Preact](https://preactjs.com) + TypeScript, ~31 KB of JS and ~13 KB of CSS, gzipped |
673
+ | Build | [esbuild](https://esbuild.github.io); the stylesheets are plain CSS |
674
+ | Tests | `go test -race ./...` |
675
+
676
+ The webapp has no framework runtime beyond Preact: routing, preferences,
677
+ popovers, and timestamp formatting are a few dozen lines each over the
678
+ platform (`history`, `localStorage`, `Intl`) rather than dependencies.
145
679
 
146
680
  # How to contribute
147
681
 
148
- This project follows the awesome [Vincent Driessen](http://nvie.com/about/) [branching model](http://nvie.com/posts/a-successful-git-branching-model/).
682
+ There is one long-lived branch, `main`. Work on a branch of your own and open
683
+ a pull request against it. Releases are cut by pushing a `v*` tag — the
684
+ version in `package.json` has to match it, or the release fails before it
685
+ publishes anything.
686
+
687
+ Before you push, run what CI runs:
149
688
 
150
- * You must add a new feature on its own branch
151
- * You must contribute to hot-fixing, directly into the master branch (and pull-request to it)
689
+ $ make ci
152
690
 
153
- This project uses JSCS to enforce a consistent code style. Your contribution must be pass jscs validation.
691
+ That is the same [`tools/ci.sh`](tools/ci.sh) the workflow calls, so a green
692
+ run here is a green run there — `go vet`, `gofmt`, `go test -race ./...`,
693
+ shellcheck on `install.sh`, the webapp typecheck and production build, the npm
694
+ packaging smoke-tested through real tarballs, and the container image. The two
695
+ things it cannot cover are the macOS server build and the Node 20 webapp
696
+ build, because both need a second machine.
154
697
 
155
- The test suite is written on top of [mochajs/mocha](http://mochajs.org/). Use the tests to check if your contribution breaks some part of the library and be sure to add new tests for each new feature.
698
+ `make` builds the toolchain image only when there is none, so after a change
699
+ to [`tools/Dockerfile`](tools/Dockerfile), `docker build -t rtail-tools tools/`
700
+ brings yours level with CI's.
156
701
 
157
- $ npm test
702
+ Add tests with each new feature, and keep the webapp type-clean — `make
703
+ typecheck` on its own is the quick version. CI builds the webapp on Node 20
704
+ and 22.
158
705
 
159
706
  ## Contributors
160
707
 
@@ -166,48 +713,52 @@ The test suite is written on top of [mochajs/mocha](http://mochajs.org/). Use th
166
713
 
167
714
  ## Roadmap (aka where you can help)
168
715
 
169
- * Write a rock solid test suite
170
- * Allow use of DTLS (waiting for node to support this https://github.com/joyent/node/pull/6704)
171
- * Add GitHub OAuth and basic auth for teams (join proposal convo here: https://github.com/kilianc/rtail/issues/44)
172
- * Implement infinite-scroll like behavior in the webapp to support bigger backlogs and make it future proof.
173
- * Publish base rtail docker image to DockerHub
174
- * Create a catch all docker logs image
175
- * Rewrite webapp using ng2
716
+ * A catch-all docker logs image: watch `docker events`, pipe every container's
717
+ `docker logs -f` into a stream named after it.
718
+ * Broaden the test suite, particularly around the UDP ingest path.
719
+
720
+ Dropped, and why:
721
+
722
+ * ~~Rewrite webapp using ng2~~ — done differently. The webapp is now Preact +
723
+ TypeScript; Angular 2 was already obsolete by the time it came up.
724
+ * ~~Publish base rtail docker image to DockerHub~~ — done, but to GHCR rather
725
+ than DockerHub, since it needs no separate account or secrets.
726
+ * ~~Write a rock solid test suite~~ — a suite exists and runs in CI. Deepening
727
+ it is the open item above.
728
+ * ~~Optional HTTP basic auth on the web port~~ — done, though as one shared
729
+ bearer token rather than basic auth; see
730
+ [Authentication](#authentication). Full OAuth
731
+ ([#44](https://github.com/kilianc/rtail/issues/44)) is still declined: it
732
+ needs a user model and session storage, which is a lot of machinery for a
733
+ single-node tool. A shared secret on UDP ingest is the one part that cannot
734
+ be done — the v1 wire format is frozen and has nowhere to carry one.
735
+ * ~~Allow use of DTLS~~ — the linked PR was against `joyent/node`, a repo that
736
+ no longer exists, and Node core still has no DTLS. Run rtail over WireGuard
737
+ or Tailscale instead; that is the modern answer to this problem.
738
+ * ~~Infinite-scroll in the webapp~~ — cannot be built as stated. The server
739
+ keeps a fixed-size ring buffer per stream and sends it in one message; there
740
+ is no pagination to scroll through. Real history means a persistence layer,
741
+ which contradicts the design. `--backlog` covers the useful part.
176
742
 
177
743
  ## Sponsors
178
- ❤ rTail? Consider sponsoring this project to keep it alive and free for the community.
744
+ ❤ rtail? Consider sponsoring this project to keep it alive and free for the community.
179
745
 
180
- * Lukibear (domain)
181
746
  * ? (wildcard TLS cert)
182
747
  * ? (.io domain)
183
748
 
184
749
  [![PayPal donate button](https://img.shields.io/badge/$_paypal-one_time_donation_➝-04cd7e.svg?style=flat-square)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=info%40rtail%2eorg&lc=US&item_name=rtail&item_number=rtail&currency_code=USD&bn=PP%2dDonationsBF%3abtn_donateCC_LG%2egif%3aNonHosted)
185
750
 
186
- <a href="mailto:info@lukibear.com">Professional support or ad-hoc is also available.</a>
187
-
188
751
  ## License
189
752
 
190
- _This software is released under the MIT license cited below_.
191
-
192
- Copyright (c) 2014 Kilian Ciuffolo, me@nailik.org. All Rights Reserved.
193
-
194
- Permission is hereby granted, free of charge, to any person
195
- obtaining a copy of this software and associated documentation
196
- files (the 'Software'), to deal in the Software without
197
- restriction, including without limitation the rights to use,
198
- copy, modify, merge, publish, distribute, sublicense, and/or sell
199
- copies of the Software, and to permit persons to whom the
200
- Software is furnished to do so, subject to the following
201
- conditions:
202
-
203
- The above copyright notice and this permission notice shall be
204
- included in all copies or substantial portions of the Software.
205
-
206
- THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
207
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
208
- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
209
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
210
- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
211
- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
212
- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
213
- OTHER DEALINGS IN THE SOFTWARE.
753
+ rtail is licensed under the **Apache License 2.0** — see [`LICENSE`](LICENSE)
754
+ and [`COPYRIGHT.md`](COPYRIGHT.md).
755
+
756
+ Run it, modify it, ship it inside your own product, commercially, at any
757
+ scale. Keep the notice, say what you changed, and the patent grant in §3
758
+ covers you. There is nothing to buy and no feature held back for a paid tier,
759
+ because there is no paid tier.
760
+
761
+ rtail **v1.x remains under the MIT licence** it was released with. That does
762
+ not change retroactively; if you are on v1, the terms you accepted still
763
+ apply. v2 spent part of its life under AGPL-3.0; that is reversed, and
764
+ [`COPYRIGHT.md`](COPYRIGHT.md) has the reasoning.