rtail 0.2.0 → 2.0.0-rc.2

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 (60) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +508 -73
  3. package/bin/rtail.js +91 -0
  4. package/package.json +15 -67
  5. package/.jscsrc +0 -28
  6. package/.jshintrc +0 -11
  7. package/.npmignore +0 -4
  8. package/.scss-lint.yml +0 -11
  9. package/cli/lib/webapp.js +0 -53
  10. package/cli/rtail-client.js +0 -156
  11. package/cli/rtail-server.js +0 -144
  12. package/dist/bundle.min.js +0 -18
  13. package/dist/css/bundle.min.css +0 -199
  14. package/dist/images/common/btn-fav-off.svg +0 -16
  15. package/dist/images/common/btn-fav-on.svg +0 -16
  16. package/dist/images/common/favicon-16x16.png +0 -0
  17. package/dist/images/common/favicon-32x32.png +0 -0
  18. package/dist/images/common/favicon-96x96.png +0 -0
  19. package/dist/images/common/icn-filter.svg +0 -16
  20. package/dist/images/common/icn-resume.svg +0 -16
  21. package/dist/images/common/logo-rtail.svg +0 -26
  22. package/dist/images/dark/btn-github.svg +0 -16
  23. package/dist/images/dark/btn-info.svg +0 -20
  24. package/dist/images/dark/btn-json-collapse.svg +0 -16
  25. package/dist/images/dark/btn-json-expand.svg +0 -16
  26. package/dist/images/dark/btn-settings.svg +0 -16
  27. package/dist/images/dark/icn-close-date.svg +0 -16
  28. package/dist/images/dark/icn-font-decrease.svg +0 -19
  29. package/dist/images/dark/icn-font-increase.svg +0 -19
  30. package/dist/images/dark/icn-font-reset.svg +0 -18
  31. package/dist/images/dark/icn-search.svg +0 -16
  32. package/dist/images/dark/icn-sort-asc.svg +0 -18
  33. package/dist/images/dark/icn-sort-desc.svg +0 -18
  34. package/dist/images/dark/icn-theme-dark.svg +0 -18
  35. package/dist/images/dark/icn-theme-light.svg +0 -26
  36. package/dist/images/dark/logo-lukibear.svg +0 -74
  37. package/dist/images/dark/logo-rtail-info.svg +0 -26
  38. package/dist/images/light/btn-github.svg +0 -16
  39. package/dist/images/light/btn-info.svg +0 -20
  40. package/dist/images/light/btn-json-collapse.svg +0 -16
  41. package/dist/images/light/btn-json-expand.svg +0 -16
  42. package/dist/images/light/btn-settings.svg +0 -16
  43. package/dist/images/light/icn-close-date.svg +0 -16
  44. package/dist/images/light/icn-font-decrease.svg +0 -19
  45. package/dist/images/light/icn-font-increase.svg +0 -19
  46. package/dist/images/light/icn-font-reset.svg +0 -18
  47. package/dist/images/light/icn-search.svg +0 -16
  48. package/dist/images/light/icn-sort-asc.svg +0 -18
  49. package/dist/images/light/icn-sort-desc.svg +0 -18
  50. package/dist/images/light/icn-theme-dark.svg +0 -18
  51. package/dist/images/light/icn-theme-light.svg +0 -26
  52. package/dist/images/light/logo-lukibear.svg +0 -74
  53. package/dist/images/light/logo-rtail-info.svg +0 -26
  54. package/dist/index.html +0 -1
  55. package/scripts/npm-publish.sh +0 -21
  56. package/scripts/set-version.sh +0 -7
  57. package/test/rtail-client.test.js +0 -131
  58. package/test/rtail-server.test.js +0 -138
  59. package/test/runner.js +0 -31
  60. package/test/util.js +0 -65
package/README.md CHANGED
@@ -1,26 +1,197 @@
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
+ > **v2 — private preview.** This branch is the rewrite described in
13
+ > [`docs/proposal-logging-system.md`](docs/proposal-logging-system.md): rTail is
14
+ > growing into a durable, searchable log store backed by Parquet and SQL. The
15
+ > server is now a single Go binary with the webapp embedded, and the npm
16
+ > package carries only the client pipe.
17
+ >
18
+ > The `cmd | rtail` wire format is frozen and always will be — the client you
19
+ > already have installed works against a v2 server unchanged. Everything else
20
+ > on this branch is subject to change until 2.0.0 ships.
21
+ >
22
+ > **Where it is:** P0–P5 are done — storage, search, compaction, the explorer
23
+ > and the ingest protocols. What is left before 2.0.0 is the operational edges:
24
+ > authentication, TLS, and a release pipeline for the three cgo platforms.
25
+
13
26
  `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
27
 
15
- ## Installation
28
+ ## Running the server
29
+
30
+ The server ships as a container image — this is the recommended way to run it.
31
+
32
+ $ docker run -d --name rtail -p 8888:8888 -p 9999:9999/udp ghcr.io/kilianc/rtail
33
+
34
+ Open <http://localhost:8888> and start piping. There is a
35
+ [`docker-compose.yml`](docker-compose.yml) if you prefer:
36
+
37
+ $ docker compose up -d
38
+
39
+ Options are flags, or `RTAIL_*` environment variables:
40
+
41
+ $ docker run -d -p 8080:8080 -p 9999:9999/udp \
42
+ -e RTAIL_WEB_PORT=8080 ghcr.io/kilianc/rtail
43
+
44
+ $ docker run -d -p 8888:8888 -p 9999:9999/udp \
45
+ ghcr.io/kilianc/rtail --backlog 500
46
+
47
+ The image binds `0.0.0.0` inside the container, so **the published ports are
48
+ reachable from your whole network**. It has no authentication — keep it on a
49
+ trusted network, or put a reverse proxy in front of it.
50
+
51
+ ## Releases
52
+
53
+ Tagged releases carry a binary for each platform and a container image.
54
+
55
+ $ docker run -p 8888:8888 -p 9999:9999/udp ghcr.io/kilianc/rtail
56
+
57
+ Or take the binary for your platform from the
58
+ [releases page](https://github.com/kilianc/rtail/releases) — `linux_amd64`,
59
+ `linux_arm64`, `darwin_amd64`, `darwin_arm64`, with a `checksums.txt` beside
60
+ them. There is no Windows build: DuckDB is cgo and nobody has asked.
61
+
62
+ The binary is not static, because DuckDB is cgo. Linux builds are linked in
63
+ Debian bookworm, so they need glibc 2.36 or newer; the container image is the
64
+ answer on anything older. Every binary is started and asked for `/healthz`
65
+ before it is published, so a release that cannot run does not become one.
66
+
67
+ ## Installing the client
16
68
 
69
+ The client is a UNIX pipe and belongs on the host whose output you are tailing,
70
+ not in the container. It is a single Go binary, about 2.6 MB, with no runtime
71
+ of any kind:
72
+
73
+ # already in the release tarball, next to rtail-server
74
+ $ tar -xzf rtail_2.0.0_darwin_arm64.tar.gz && sudo mv rtail /usr/local/bin
75
+
76
+ # or, if you have Go
77
+ $ go install github.com/kilianc/rtail/v2/cmd/rtail@latest
78
+
79
+ # or, if you are already standing in a project
17
80
  $ npm install -g rtail
18
81
 
19
- ## Web app
82
+ The npm route is a convenience, not a dependency: the package holds a short
83
+ launcher and one optional dependency per platform, each carrying a prebuilt
84
+ binary and declaring the `os` and `cpu` it is for. npm downloads the one that
85
+ matches and skips the rest. There is no postinstall script and nothing is
86
+ fetched at install time, so it works behind a proxy and under
87
+ `--ignore-scripts`. The launcher needs Node 20 or newer; the other two routes
88
+ need nothing.
89
+
90
+ Windows is published to npm and not as a tarball — the client cross-compiles
91
+ anywhere, the server does not.
92
+
93
+ ## The explorer
94
+
95
+ ![The rTail explorer](docs/media/explorer.png)
96
+
97
+ *A spike of upstream timeouts, twenty minutes back: `level>=WARN` on the `api`
98
+ stream, with `service`, `latency_ms` and `region` promoted to columns.*
99
+
100
+ **One filter drives everything.** Type it and the live tail narrows — the
101
+ server compiles the same rQL to a predicate over the ingest stream. Switch to
102
+ history and the same query runs against Parquet. Same semantics either way,
103
+ because there is one parser behind both.
104
+
105
+ **The histogram is the navigation.** Counts over time stacked by severity; drag
106
+ across it to zoom the range.
107
+
108
+ **Most queries are built by clicking.** Expand any event and every value
109
+ carries "filter to this" and "exclude this"; the command bar updates to match,
110
+ so whatever you built by pointing is something you could have typed. The field
111
+ sidebar shows the top values of every field *within the current results*, which
112
+ answers "what is even in here" before you know what to search for.
113
+
114
+ **Completion is real.** It comes from the catalog's key inventory — every field
115
+ that has actually appeared, with its type and how often — not from a guess at
116
+ what a log line might contain.
117
+
118
+ **Every view is a link.** Stream, filter, time range, promoted columns, and
119
+ whether the timeline is open — all of it lives in the URL, so the screenshot
120
+ above is a URL somebody could send you.
121
+
122
+ **Any field can become a column.** The ⊞ beside a field in the sidebar pulls it
123
+ out of the summary into a column of its own, and the × on the header puts it
124
+ back.
125
+
126
+ **The query pane holds everything that acts on the query** — the language
127
+ switch, the examples, the time range and Run — inside its own border, because
128
+ they belong to the query rather than to the page.
20
129
 
21
- ![](https://s3.amazonaws.com/rtail/github/dark.png)
130
+ Keyboard: `/` or `⌘K` focuses the editor, `⌘⏎` runs it (plain `⏎` is a
131
+ newline — it is an editor), `j`/`k` move between events, `⎋` collapses
132
+ everything.
22
133
 
23
- ![](https://s3.amazonaws.com/rtail/github/light.png)
134
+ ## Getting logs in
135
+
136
+ Four ways, and every one of them lands in the same envelope — a record is
137
+ indistinguishable once stored, whichever door it came through.
138
+
139
+ **UNIX pipes**, the original, frozen forever:
140
+
141
+ $ node server.js 2>&1 | rtail --id api.example.com
142
+
143
+ **HTTP**, for anything that would mind losing a line. UDP is at-most-once and
144
+ capped at one datagram; this acknowledges, so a 200 means the records are in
145
+ the write-ahead log and anything else is safe to retry:
146
+
147
+ $ curl -XPOST localhost:8888/v1/ingest?stream=api --data-binary @- <<'JSON'
148
+ {"level":"error","msg":"upstream timeout","service":"api","latency_ms":30004}
149
+ a plain line works too
150
+ JSON
151
+
152
+ Newline-delimited JSON or plain text — the content type is not consulted,
153
+ because the normalizer already decides per line what it is looking at. `gzip`
154
+ and `zstd` bodies are decoded.
155
+
156
+ **OpenTelemetry**, protobuf or JSON, at `/v1/logs`. Point any collector at it
157
+ with a config line and no code change:
158
+
159
+ exporters:
160
+ otlphttp:
161
+ logs_endpoint: http://rtail:8888/v1/logs
162
+
163
+ `service.name` becomes the stream, log and resource attributes become fields,
164
+ and `trace_id`/`span_id` are promoted so correlation actually works.
165
+
166
+ **Syslog**, RFC5424 and RFC3164, UDP and TCP on the same port:
167
+
168
+ $ rtail-server --data ./logs --syslog-port 5140
169
+
170
+ # rsyslog.conf
171
+ *.* @@rtail-host:5140;RSYSLOG_SyslogProtocol23Format
172
+
173
+ APP-NAME becomes the stream, the priority becomes a real severity — so
174
+ `level>=ERROR` works on syslog without anyone writing a parser — and RFC5424
175
+ structured data is promoted to fields.
176
+
177
+ Anything that can write NDJSON to an HTTP endpoint works without a plugin:
178
+
179
+ # vector.toml
180
+ [sinks.rtail]
181
+ type = "http"
182
+ inputs = ["my_source"]
183
+ uri = "http://rtail:8888/v1/ingest?stream=vector"
184
+ encoding.codec = "json"
185
+ framing.method = "newline_delimited"
186
+
187
+ # fluent-bit.conf
188
+ [OUTPUT]
189
+ Name http
190
+ Match *
191
+ Host rtail
192
+ Port 8888
193
+ URI /v1/ingest?stream=fluentbit
194
+ Format json_lines
24
195
 
25
196
  ## Rationale
26
197
 
@@ -33,9 +204,126 @@ There are many log aggregation tools out there, but few of them are realtime. **
33
204
  **The `rtail` approach is very simple:**
34
205
  * 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
206
  * broadcast every line using UDP
36
- * `rtail-server`, **if listening**, will dispatch the stream into your browser, using [socket.io](http://socket.io/).
207
+ * `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).
208
+
209
+ `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.
210
+
211
+ ## Storage
212
+
213
+ $ rtail-server --data ./logs
214
+
215
+ Point `--data` at a directory and every line is written to Parquet. Without it,
216
+ rTail is what it always was: an in-memory ring, nothing on disk, nothing to
217
+ clean up.
218
+
219
+ **There is no schema to define.** The envelope — timestamp, stream, level,
220
+ message, host, sequence, and the original line — is always there. Everything
221
+ else is whatever your logs happened to contain: each root-level JSON key
222
+ becomes its own typed column, and nested objects are kept as JSON you can
223
+ path into. Two files written a week apart with completely different keys
224
+ reconcile by name at read time.
225
+
226
+ **Your logs are just files.** The layout is plain and browsable:
227
+
228
+ logs/
229
+ catalog.sqlite
230
+ wal/
231
+ streams/api.example.com/2026/08/11/L0-…parquet
232
+
233
+ which means any tool reads them, with rTail uninstalled and nothing exported:
234
+
235
+ $ duckdb -c "
236
+ SELECT a_service, count(*)
237
+ FROM read_parquet('logs/streams/**/*.parquet', union_by_name := true)
238
+ WHERE level = 'ERROR' AND a_latency_ms > 500
239
+ GROUP BY 1"
240
+
241
+ That is the point, and it is covered by a test rather than merely claimed.
242
+
243
+ **Durability.** Lines land in a write-ahead log before anything else, and are
244
+ batched into Parquet from there. The WAL is fsynced every 100ms, so an
245
+ unclean shutdown costs at most that; a clean one costs nothing. Recovery
246
+ replays whatever the previous process did not finish, and is idempotent —
247
+ crashing midway through a flush and restarting produces the same files, not
248
+ duplicates.
249
+
250
+ **Compaction.** A flush every thirty seconds per stream is thousands of tiny
251
+ files a day, each with a footer to open. A background pass merges them:
37
252
 
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/).
253
+ | level | holds | sorted by |
254
+ |---|---|---|
255
+ | L0 | one flush | `seq` — the order it arrived in |
256
+ | L1 | an hour | `ts` |
257
+ | L2 | a day | `(level, ts)`, or `--cluster-by` |
258
+
259
+ Sort order is the point, not the file count. Sorting by time makes range
260
+ pruning exact at row-group granularity rather than merely likely, and a
261
+ low-cardinality leading column turns `level>=ERROR` over a day into a
262
+ row-group skip instead of a scan. Compaction is also where a messy population
263
+ of schemas becomes one clean union — types that disagreed get widened,
264
+ always-null columns disappear.
265
+
266
+ In practice six flushes of a small stream merge from 24KB to 5KB.
267
+
268
+ A merge either publishes its output and retires its inputs, or changes
269
+ nothing. Retired files stay on disk for a grace period, so a query that
270
+ started before the merge still has its files. `--compact-interval 0` turns the
271
+ whole thing off.
272
+
273
+ **Retention** is per-server, and all of it happens during compaction:
274
+
275
+ $ rtail-server --data ./logs \
276
+ --retention 30d \
277
+ --retention-raw 7d \
278
+ --downsample-after 14d --downsample-level WARN
279
+
280
+ `--retention` deletes whole files once their newest record is past the
281
+ horizon; a file straddling the boundary survives until all of it has expired,
282
+ because rewriting a file to drop a few rows costs far more than keeping them.
283
+ `--retention-raw` drops the original line while keeping every promoted column,
284
+ which roughly halves the footprint — the cost is that keys which were never
285
+ promoted stop being queryable, since `raw` was their fallback.
286
+ `--downsample-after` discards anything below `--downsample-level` once it is
287
+ old enough.
288
+
289
+ Durations take `d` and `w` as well as Go's own units, because `--retention
290
+ 720h` is nobody's idea of clear.
291
+
292
+ ## Search
293
+
294
+ Two ways in, sharing one set of semantics.
295
+
296
+ **The filter bar** — terse, and what most searching looks like:
297
+
298
+ level>=ERROR service=api "connection timeout"
299
+ -path:/health AND (region=us-east-1 OR region=us-west-2)
300
+ latency_ms>500 req.path:~"^/v1/"
301
+ trace_id=*
302
+
303
+ `k=v` is equality, `k:v` a case-insensitive substring, `k:~re` a regular
304
+ expression, `k>n` a comparison, `k=*` "the key exists", `k=null` "it does not".
305
+ A bare word is full-text across the message and the original line. Terms are
306
+ ANDed unless you write `OR`; `-` and `NOT` negate; parentheses group.
307
+
308
+ `level` compares by **severity, not alphabetically** — `level>=ERROR` matches
309
+ FATAL too, which is what you meant. Quoting a value suppresses type coercion,
310
+ so `status=200` is the number and `status="200"` is the string.
311
+
312
+ **Raw SQL**, for everything else:
313
+
314
+ curl -XPOST localhost:8888/v1/sql -d '{
315
+ "sql": "SELECT a_service, count(*) n, quantile_cont(a_latency_ms, 0.95) p95
316
+ FROM logs WHERE level = '"'"'ERROR'"'"' GROUP BY 1 ORDER BY n DESC",
317
+ "from": "-6h"
318
+ }'
319
+
320
+ `logs` is a view over the pruned files. The query runs read-only in a DuckDB
321
+ sandbox that can see nothing but your data directory.
322
+
323
+ **The time range is the partition pruner**, not decoration. It is always
324
+ applied — a query without one gets the last 24 hours injected — and every
325
+ response reports the window it used along with how many files and rows the
326
+ query implied reading, so a slow search can explain itself.
39
327
 
40
328
  ## Examples
41
329
 
@@ -70,11 +358,10 @@ For fun and debugging:
70
358
 
71
359
  Options:
72
360
  --host, -h The server host [string] [default: "127.0.0.1"]
73
- --port, -p The server port [string] [default: 9999]
361
+ --port, -p The server port [number] [default: 9999]
74
362
  --id, --name The log stream id [string] [default: (moniker)]
75
363
  --mute, -m Don't pipe stdin with stdout [boolean]
76
364
  --tty Keeps ansi colors [boolean] [default: true]
77
- --parse-date Looks for dates to use as timestamp [boolean] [default: true]
78
365
  --help Show help [boolean]
79
366
  --version, -v Show version number [boolean]
80
367
 
@@ -85,12 +372,10 @@ For fun and debugging:
85
372
  server | rtail --port 43567 Uses custom port
86
373
  server | rtail --mute No stdout
87
374
  server | rtail --no-tty Strips ansi colors
88
- server | rtail --no-date-parse Disable date parsing/stripping
89
-
90
375
 
91
376
  ## `rtail-server(1)`
92
377
 
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.**
378
+ `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 static Go binary with the webapp embedded, pushing every incoming UDP message to the browser over server-sent events.**
94
379
 
95
380
  There is little to no configuration – The default UDP/HTTP ports can be changed, but that's it.
96
381
 
@@ -100,17 +385,13 @@ Use default values:
100
385
 
101
386
  $ rtail-server
102
387
 
103
- Always use latest, stable webapp:
104
-
105
- $ rtail-server --web-version stable
106
-
107
388
  Use custom ports:
108
389
 
109
390
  $ rtail-server --web-port 8080 --udp-port 9090
110
391
 
111
392
  Set debugging on:
112
393
 
113
- $ DEBUG=rtail:* rtail-server
394
+ $ rtail-server --verbose
114
395
 
115
396
  Open your browser and start tailing logs!
116
397
 
@@ -119,29 +400,176 @@ Open your browser and start tailing logs!
119
400
  $ rtail-server --help
120
401
  Usage: rtail-server [OPTIONS]
121
402
 
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
403
+ Every option is also settable as RTAIL_*, e.g. RTAIL_WEB_PORT.
404
+
405
+ -udp-host string the listening UDP hostname (default "127.0.0.1")
406
+ -udp-port int the listening UDP port (default 9999)
407
+ -web-host string the listening HTTP hostname (default "127.0.0.1")
408
+ -web-port int the listening HTTP port (default 8888)
409
+ -backlog int lines of history kept per stream (default 100)
410
+ -data string directory for durable storage (unset: memory only)
411
+ -web-root string serve the webapp from this directory instead of the binary
412
+ -auth-token string require this bearer token on the API and webapp
413
+ -tls-cert string serve HTTPS with this certificate (PEM)
414
+ -tls-key string the private key for --tls-cert (PEM)
415
+ -insecure allow a non-loopback address with no --auth-token
416
+ -verbose log at debug level
417
+ -version print the version and exit
418
+
419
+ The v1 short aliases still work: `--uh`, `--up`, `--wh`, `--wp`, `-b`.
420
+
421
+ Every option can also be set as an environment variable, prefixed with
422
+ `RTAIL_` — `RTAIL_WEB_PORT=8080`, `RTAIL_BACKLOG=500`, and so on. This is how
423
+ the container image is configured.
424
+
425
+ Note that `--backlog` is about the *live view* — how many recent lines a
426
+ freshly opened browser tab is handed. It has nothing to do with how much is
427
+ kept on disk, which is everything until retention exists.
428
+
429
+ ## API
430
+
431
+ The webapp is a client of a small documented API, so anything else can be too:
432
+
433
+ | endpoint | what it does |
434
+ |---|---|
435
+ | `GET /v1/streams` | every known stream |
436
+ | `GET /v1/tail?stream=<name>` | SSE feed: `streams`, `backlog`, then `line` events. Omit `stream` to watch the stream list only |
437
+ | `GET /v1/search` | matching records, keyset-paginated |
438
+ | `GET /v1/histogram` | match counts over time, split by severity |
439
+ | `GET /v1/fields?field=<name>` | the most common values of a field, within the current filter |
440
+ | `GET /v1/schema` | every key ever seen, with its type — what autocomplete completes against |
441
+ | `POST /v1/sql` | read-only SQL over a `logs` view |
442
+ | `POST /v1/ingest` | NDJSON or plain lines, optionally gzip/zstd |
443
+ | `POST /v1/logs` | OTLP logs, protobuf or JSON |
444
+ | `GET /healthz` | version, stream count, per-receiver ingest counters |
445
+
446
+ The search endpoints share their parameters: `q` (filter), `stream`, `from` and
447
+ `to` (RFC3339, epoch millis, or a relative `-6h`), `limit`, `cursor`, `order`.
448
+ They return 503 with an explanation when the server is running without
449
+ `--data`, since there is nothing to search.
137
450
 
138
451
  ## UDP Broadcasting
139
452
 
140
453
  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
454
 
142
- ## Authentication layer
455
+ ## Authentication
456
+
457
+ One shared token, or nothing.
458
+
459
+ $ rtail-server --auth-token "$(openssl rand -hex 32)" --web-host 0.0.0.0
460
+
461
+ With a token set, every route except `/healthz` requires it. Programs send it
462
+ as a header; browsers sign in once and get a cookie.
463
+
464
+ $ curl -H "Authorization: Bearer $TOKEN" http://logs.example.com/v1/streams
465
+
466
+ `X-Rtail-Token` works too, for clients that reserve `Authorization`. In a
467
+ browser, opening the server without one gives you a sign-in form, and
468
+ `https://logs.example.com/?token=…` signs you in from a link — the token is
469
+ spent immediately and redirected out of the address bar so it does not end up
470
+ in your history.
471
+
472
+ `/healthz` is deliberately left open, so a container healthcheck or a load
473
+ balancer keeps working without being handed the secret. It reports liveness
474
+ and stream names; everything that could show a log line is behind the token.
475
+
476
+ **Running with no token is refused on any address but loopback.** This is the
477
+ one rule the server enforces on your behalf:
143
478
 
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.
479
+ $ rtail-server --web-host 0.0.0.0
480
+ rtail-server: refusing to listen on 0.0.0.0 with no --auth-token:
481
+ anyone who can reach it could read every log.
482
+
483
+ Pass `--insecure` if something in front of it handles access. Nothing stops
484
+ you; it just will not happen by accident.
485
+
486
+ What this deliberately is not: accounts, roles, OIDC, SAML, or per-stream
487
+ permissions. rTail is a single-node tool, and anyone who needs those already
488
+ has a reverse proxy that does them properly.
489
+
490
+ **Two holes worth knowing about.** UDP ingest is unauthenticated, because UDP
491
+ has no handshake to carry a token and v1's wire format is frozen. Syslog is
492
+ unauthenticated for the same reason. Both listen on `127.0.0.1` by default; if
493
+ you move either off loopback you are trusting your network.
494
+
495
+ ### TLS
496
+
497
+ $ rtail-server --tls-cert cert.pem --tls-key key.pem
498
+
499
+ Both or neither — half a pair is refused at startup rather than failing on
500
+ somebody's first request. There is no ACME client and there will not be one:
501
+ a certificate is the one thing every reverse proxy already solves.
502
+
503
+ # Running it locally
504
+
505
+ Note there are two images, and they are not the same thing:
506
+ [`Dockerfile`](Dockerfile) is the server you deploy, and
507
+ [`tools/Dockerfile`](tools/Dockerfile) below is the development toolchain,
508
+ which mounts your checkout.
509
+
510
+ The toolchain lives in a container ([`tools/Dockerfile`](tools/Dockerfile)), so Docker is
511
+ the only thing you need installed — no Node.js, no Go, no npm, no global CLIs.
512
+ v2 needs both toolchains: Node builds the webapp, Go builds the server that
513
+ embeds it, and the image carries pinned versions of each.
514
+
515
+ $ make dev
516
+
517
+ That builds the assets and the server binary, starts `rtail-server`, feeds it
518
+ three live demo streams, and serves the webapp. It prints the URL — `make url`
519
+ prints it again. Stylesheets and the bundle recompile on save; reload the
520
+ browser to pick them up. Go changes need a `make dev` restart, because the
521
+ binary is rebuilt at startup. `Ctrl-C` stops everything.
522
+
523
+ The first run also builds the toolchain image and installs dependencies, so it
524
+ takes a minute; subsequent runs start immediately.
525
+
526
+ To leave it running in the background instead:
527
+
528
+ $ make up # start detached, wait for it, print the URL
529
+ $ make logs # tail it
530
+ $ make down # stop it
531
+
532
+ Other targets:
533
+
534
+ $ make server # build bin/rtail-server
535
+ $ make release # build the self-contained server, webapp embedded
536
+ $ make build # build the webapp into app/ (development)
537
+ $ make dist # build the minified webapp into web/dist
538
+ $ make test # run the Go and Node test suites
539
+ $ make vet # go vet
540
+ $ make fmt # gofmt the Go sources
541
+ $ make typecheck # type-check the webapp
542
+ $ make shell # open a shell inside the toolchain container
543
+ $ make clean # remove generated assets, binaries and dependencies
544
+
545
+ ### Ports
546
+
547
+ Ports are derived from the worktree path, so several checkouts of this repo can
548
+ run at once without colliding. Each gets its own HTTP port, UDP port, and
549
+ container name, fixed across runs — sharing any of the three fails confusingly,
550
+ since two servers can both bind the same UDP port and the loser simply never
551
+ receives a log line.
552
+
553
+ `make url` reports the current one. Override with `make dev PORT=9000
554
+ UDP_PORT=9001`.
555
+
556
+ If you do have a Node.js toolchain on your machine, the same targets are plain
557
+ npm scripts — `npm install && npm run dev`. Node builds the webapp and is not
558
+ in the product: the server embeds the bundle, and the client is a Go binary.
559
+
560
+ ### The stack
561
+
562
+ | | |
563
+ | --- | --- |
564
+ | Client | Go, one static binary, [json5](https://github.com/titanous/json5) its only dependency |
565
+ | Server | Go + [DuckDB](https://duckdb.org) + [parquet-go](https://github.com/parquet-go/parquet-go) |
566
+ | Webapp | [Preact](https://preactjs.com) + TypeScript, ~33 KB gzipped |
567
+ | Build | [esbuild](https://esbuild.github.io); the stylesheets are plain CSS |
568
+ | Tests | `go test ./...` |
569
+
570
+ The webapp has no framework runtime beyond Preact: routing, preferences,
571
+ popovers, and timestamp formatting are a few dozen lines each over the
572
+ platform (`history`, `localStorage`, `Intl`) rather than dependencies.
145
573
 
146
574
  # How to contribute
147
575
 
@@ -150,11 +578,17 @@ This project follows the awesome [Vincent Driessen](http://nvie.com/about/) [bra
150
578
  * You must add a new feature on its own branch
151
579
  * You must contribute to hot-fixing, directly into the master branch (and pull-request to it)
152
580
 
153
- This project uses JSCS to enforce a consistent code style. Your contribution must be pass jscs validation.
581
+ The test suite is `make test`, which is `go test -race ./...` inside the
582
+ toolchain image. Use it to check whether your contribution breaks some part of
583
+ the library, and be sure to add new tests for each new feature.
584
+
585
+ $ make test
586
+
587
+ The webapp is TypeScript; please keep it type-clean:
154
588
 
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.
589
+ $ make typecheck
156
590
 
157
- $ npm test
591
+ CI runs both, plus the production build, on Node 20 and 22.
158
592
 
159
593
  ## Contributors
160
594
 
@@ -166,48 +600,49 @@ The test suite is written on top of [mochajs/mocha](http://mochajs.org/). Use th
166
600
 
167
601
  ## Roadmap (aka where you can help)
168
602
 
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
603
+ * Optional HTTP basic auth on the web port, and a shared secret on UDP ingest.
604
+ Full OAuth ([#44](https://github.com/kilianc/rtail/issues/44)) needs a user
605
+ model and session storage, which is a lot of machinery for a tool with no
606
+ persistence layer — basic auth covers the actual risk.
607
+ * A catch-all docker logs image: watch `docker events`, pipe every container's
608
+ `docker logs -f` into a stream named after it.
609
+ * Broaden the test suite, particularly around the UDP ingest path.
610
+
611
+ Dropped, and why:
612
+
613
+ * ~~Rewrite webapp using ng2~~ — done differently. The webapp is now Preact +
614
+ TypeScript; Angular 2 was already obsolete by the time it came up.
615
+ * ~~Publish base rtail docker image to DockerHub~~ — done, but to GHCR rather
616
+ than DockerHub, since it needs no separate account or secrets.
617
+ * ~~Write a rock solid test suite~~ — a suite exists and runs in CI. Deepening
618
+ it is the open item above.
619
+ * ~~Allow use of DTLS~~ — the linked PR was against `joyent/node`, a repo that
620
+ no longer exists, and Node core still has no DTLS. Run rtail over WireGuard
621
+ or Tailscale instead; that is the modern answer to this problem.
622
+ * ~~Infinite-scroll in the webapp~~ — cannot be built as stated. The server
623
+ keeps a fixed-size ring buffer per stream and sends it in one message; there
624
+ is no pagination to scroll through. Real history means a persistence layer,
625
+ which contradicts the design. `--backlog` covers the useful part.
176
626
 
177
627
  ## Sponsors
178
628
  ❤ rTail? Consider sponsoring this project to keep it alive and free for the community.
179
629
 
180
- * Lukibear (domain)
181
630
  * ? (wildcard TLS cert)
182
631
  * ? (.io domain)
183
632
 
184
633
  [![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
634
 
186
- <a href="mailto:info@lukibear.com">Professional support or ad-hoc is also available.</a>
187
-
188
635
  ## License
189
636
 
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.
637
+ rTail is licensed under the **Apache License 2.0** — see [`LICENSE`](LICENSE)
638
+ and [`COPYRIGHT.md`](COPYRIGHT.md).
639
+
640
+ Run it, modify it, ship it inside your own product, commercially, at any
641
+ scale. Keep the notice, say what you changed, and the patent grant in §3
642
+ covers you. There is nothing to buy and no feature held back for a paid tier,
643
+ because there is no paid tier.
644
+
645
+ rTail **v1.x remains under the MIT licence** it was released with. That does
646
+ not change retroactively; if you are on v1, the terms you accepted still
647
+ apply. v2 spent part of its life under AGPL-3.0; that is reversed, and
648
+ [`COPYRIGHT.md`](COPYRIGHT.md) has the reasoning.