rtail 0.2.1 → 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.
- package/LICENSE +202 -0
- package/README.md +508 -73
- package/bin/rtail.js +91 -0
- package/package.json +15 -67
- package/.jscsrc +0 -28
- package/.jshintrc +0 -11
- package/.npmignore +0 -4
- package/.scss-lint.yml +0 -11
- package/cli/lib/webapp.js +0 -54
- package/cli/rtail-client.js +0 -156
- package/cli/rtail-server.js +0 -144
- package/dist/bundle.min.js +0 -18
- package/dist/css/bundle.min.css +0 -199
- package/dist/images/common/btn-fav-off.svg +0 -16
- package/dist/images/common/btn-fav-on.svg +0 -16
- package/dist/images/common/favicon-16x16.png +0 -0
- package/dist/images/common/favicon-32x32.png +0 -0
- package/dist/images/common/favicon-96x96.png +0 -0
- package/dist/images/common/icn-filter.svg +0 -16
- package/dist/images/common/icn-resume.svg +0 -16
- package/dist/images/common/logo-rtail.svg +0 -26
- package/dist/images/dark/btn-github.svg +0 -16
- package/dist/images/dark/btn-info.svg +0 -20
- package/dist/images/dark/btn-json-collapse.svg +0 -16
- package/dist/images/dark/btn-json-expand.svg +0 -16
- package/dist/images/dark/btn-settings.svg +0 -16
- package/dist/images/dark/icn-close-date.svg +0 -16
- package/dist/images/dark/icn-font-decrease.svg +0 -19
- package/dist/images/dark/icn-font-increase.svg +0 -19
- package/dist/images/dark/icn-font-reset.svg +0 -18
- package/dist/images/dark/icn-search.svg +0 -16
- package/dist/images/dark/icn-sort-asc.svg +0 -18
- package/dist/images/dark/icn-sort-desc.svg +0 -18
- package/dist/images/dark/icn-theme-dark.svg +0 -18
- package/dist/images/dark/icn-theme-light.svg +0 -26
- package/dist/images/dark/logo-lukibear.svg +0 -74
- package/dist/images/dark/logo-rtail-info.svg +0 -26
- package/dist/images/light/btn-github.svg +0 -16
- package/dist/images/light/btn-info.svg +0 -20
- package/dist/images/light/btn-json-collapse.svg +0 -16
- package/dist/images/light/btn-json-expand.svg +0 -16
- package/dist/images/light/btn-settings.svg +0 -16
- package/dist/images/light/icn-close-date.svg +0 -16
- package/dist/images/light/icn-font-decrease.svg +0 -19
- package/dist/images/light/icn-font-increase.svg +0 -19
- package/dist/images/light/icn-font-reset.svg +0 -18
- package/dist/images/light/icn-search.svg +0 -16
- package/dist/images/light/icn-sort-asc.svg +0 -18
- package/dist/images/light/icn-sort-desc.svg +0 -18
- package/dist/images/light/icn-theme-dark.svg +0 -18
- package/dist/images/light/icn-theme-light.svg +0 -26
- package/dist/images/light/logo-lukibear.svg +0 -74
- package/dist/images/light/logo-rtail-info.svg +0 -26
- package/dist/index.html +0 -1
- package/scripts/npm-publish.sh +0 -21
- package/scripts/set-version.sh +0 -7
- package/test/rtail-client.test.js +0 -131
- package/test/rtail-server.test.js +0 -138
- package/test/runner.js +0 -31
- package/test/util.js +0 -65
package/README.md
CHANGED
|
@@ -1,26 +1,197 @@
|
|
|
1
1
|
# `rtail(1)`
|
|
2
2
|
|
|
3
|
-
[](https://coveralls.io/r/kilianc/rtail)
|
|
3
|
+
[](https://github.com/kilianc/rtail/actions/workflows/ci.yml)
|
|
5
4
|
[](https://www.npmjs.com/package/rtail)
|
|
6
5
|
[](https://www.npmjs.com/package/rtail)
|
|
7
6
|
[](https://github.com/kilianc/rtail)
|
|
8
|
-
[](LICENSE)
|
|
9
8
|
[](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
|
-
##
|
|
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
|
-
|
|
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
|
+

|
|
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
|
-
|
|
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
|
-
|
|
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 [
|
|
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
|
-
|
|
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 [
|
|
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
|
|
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
|
-
$
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
589
|
+
$ make typecheck
|
|
156
590
|
|
|
157
|
-
|
|
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
|
-
*
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
*
|
|
174
|
-
|
|
175
|
-
*
|
|
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
|
[](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=info%40rtail%2eorg&lc=US&item_name=rtail&item_number=rtail¤cy_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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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.
|