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.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +633 -82
- package/bin/rtail.js +97 -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,256 @@
|
|
|
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
|
+
<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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
142
|
+
Windows is published to npm and not as a tarball — the client cross-compiles
|
|
143
|
+
anywhere, the server does not.
|
|
20
144
|
|
|
21
|
-
|
|
145
|
+
## The explorer
|
|
22
146
|
|
|
23
|
-
|
|
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 [
|
|
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
|
-
|
|
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 [
|
|
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
|
|
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
|
-
|
|
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
|
-
$
|
|
459
|
+
$ rtail-server --verbose
|
|
114
460
|
|
|
115
461
|
Open your browser and start tailing logs!
|
|
116
462
|
|
|
117
463
|
## Params
|
|
118
464
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
|
549
|
+
## Authentication
|
|
550
|
+
|
|
551
|
+
One shared token, or nothing.
|
|
143
552
|
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
* You must contribute to hot-fixing, directly into the master branch (and pull-request to it)
|
|
689
|
+
$ make ci
|
|
152
690
|
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
170
|
-
|
|
171
|
-
*
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
❤
|
|
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
|
[](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
750
|
|
|
186
|
-
<a href="mailto:info@lukibear.com">Professional support or ad-hoc is also available.</a>
|
|
187
|
-
|
|
188
751
|
## License
|
|
189
752
|
|
|
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.
|
|
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.
|