Real-time, tail -f-style viewer for Amazon CloudWatch Logs, rendered in a
browser terminal. Backed by the CloudWatch Logs
Live Tail
(StartLiveTail) API, with fan-out so multiple people can watch the same log
group over a single session.
browser (xterm.js) --WebSocket--> clterm --StartLiveTail--> CloudWatch Logs
hub: one session per log group,
fanned out to every viewer
- One Live Tail session per log group, shared across all viewers (Live Tail is billed per minute, so we never open one session per viewer).
- Sessions are opened lazily on the first viewer and closed shortly after the
last one leaves (
-linger), so idle groups stop billing. - The 3-hour Live Tail session cap is handled by automatic reconnection.
go run . # listens on :8080
# then open
open 'http://localhost:8080/tail?group=/aws/lambda/your-fn'Flags:
| flag | default | meaning |
|---|---|---|
-addr |
:8080 |
listen address |
-region |
(from config) | AWS region; overrides AWS_REGION |
-linger |
15s |
keep a session open this long after the last viewer leaves |
Standard AWS credential and region resolution applies (AWS_PROFILE,
AWS_REGION, env vars, SSO, instance role, ...) via the default config chain.
-region overrides the resolved region. A region must be set one way or the
other, or startup fails.
Multi-arch images (amd64/arm64) are published to
ghcr.io/winebarrel/clterm on each release.
# mount ~/.aws
docker run --rm -p 8080:8080 \
-e AWS_REGION=ap-northeast-1 \
-v ~/.aws:/home/nonroot/.aws -e AWS_PROFILE=your-profile \
ghcr.io/winebarrel/clterm:latest
# or pass credentials as env vars
docker run --rm -p 8080:8080 \
-e AWS_REGION=ap-northeast-1 \
-e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN \
ghcr.io/winebarrel/clterm:latest
# then open http://localhost:8080/tail?group=/aws/lambda/your-fn/tail?group=<log-group> the terminal page
/ws?group=<log-group> the WebSocket stream
/ws?group=<log-group>®ion=<region> tail a group in another region
/ws?group=<log-group>&filter=... raw CloudWatch Logs filter pattern
/ws?group=<log-group>&q=... literal search (auto-quoted)
/ws?group=<log-group>&since=1h replay recent history, then tail
/tail?group=<log-group>&time=1&stream=1 prepend the timestamp / stream name
By default the /tail page shows just the raw log message (e.g.
END RequestId: ...). The timestamp and log stream name are not part of the
message — the API returns them as separate fields — so they are opt-in:
time=1 prepends the timestamp, stream=1 prepends the
2026/07/27/[$LATEST]... stream name (each also accepts true/yes/on).
filter is a raw CloudWatch Logs filter pattern
(supports -exclude, multiple AND terms, { $.level = "ERROR" }, ...). If you
just want a plain substring and don't want to think about the syntax, use q
instead — it is quoted and escaped for you, so q=aaaa- matches the literal
aaaa-. When both are given, filter wins. Both apply to the history replay
and the live tail alike.
since takes a Go duration, extended with d (day) and w (week) units —
5m, 1h, 1d, 1w3d12h, ... Before the live tail starts, past events in
[now-since, now] are fetched with FilterLogEvents and replayed oldest-first
(capped at 10,000 events, oldest kept; the same filter applies). It works on
the page too, e.g. /tail?group=/aws/lambda/foo&since=1d.
The log group is passed as the group query parameter, e.g.
/tail?group=/aws/lambda/foo. StartLiveTail requires an ARN, so a bare name
is resolved to arn:<partition>:logs:<region>:<account>:log-group:<name> using
the caller's account and — unless region is given — the configured region.
Add region=<region> to tail a group in another region; a per-region client is
created and cached automatically. To tail a group in another account, pass
its full ARN as group instead (the ARN's own region is used).
The process needs:
[
{ "Effect": "Allow", "Action": "logs:StartLiveTail", "Resource": "*" },
{ "Effect": "Allow", "Action": "logs:FilterLogEvents", "Resource": "*" },
{ "Effect": "Allow", "Action": "sts:GetCallerIdentity", "Resource": "*" }
]sts:GetCallerIdentity (called once at startup) is used to build log group
ARNs from bare names. logs:FilterLogEvents is only needed if you use the
since history replay. Scope the logs:* Resources to the log group ARNs
you want to expose.
- Live Tail samples events above ~500/sec; a notice is shown when that happens.
- On reconnect (3h cap or transient errors) a few events around the gap may be
missed —
tail -fsemantics, not a gap-free guarantee. - Slow WebSocket clients have their backlog dropped (with a count) rather than stalling the shared hub.
- No authorization is built in: anyone who can reach the server can tail any group the process's IAM role allows. Put it behind your own authz.
- The frontend (xterm.js) is vendored under
web/vendor/and embedded into the binary, so there is no CDN dependency — it runs fully offline / in closed networks. Those assets are MIT-licensed (seeweb/vendor/xterm.LICENSE).
