$ logcrux /var/log/syslog
$ logcrux /var/log/nginx/ # every log in a directory
$ logcrux /var/log/syslog /var/log/auth.log # several files
Or pipe a log in. stdin is read to the end before analysis, so pipe a
bounded slice, not a stream that never ends (tail -f):
$ journalctl -b | logcrux
$ journalctl -u nginx --since "1 hour ago" | logcrux
$ docker logs myapp 2>&1 | logcrux
$ kubectl logs deploy/api --previous | logcrux
$ tail -n 20000 /var/log/app.log | logcrux
This page writes logcrux ... everywhere, which assumes
the executable is on your PATH. Whether it is depends on
how you installed it:
| installed with | invoke as |
|---|---|
uv tool install or pipx | logcrux ..., already on PATH |
pip into a venv | ~/.venvs/logcrux/bin/logcrux ..., or activate the venv first |
| no install, one-off run | uvx --python 3.12 logcrux ... |
Everything else on this page, flags, stdin, exit codes, is identical
regardless of which form you use; substitute your invocation for the
literal logcrux. See install
for the full picture, including hosts where logcrux can't be installed
at all, analyze those from outside instead:
docker logs oldcontainer | logcrux, run from a host that
does have it installed.
PATH ... | one or more files or directories (a directory is read one level deep; binary files, documents and config files are skipped and listed). Omit, or pass -, to read stdin |
|---|---|
--last DURATION | only analyze events from the trailing window, e.g. --last 1h, --last 30m, --last 2d. Timestamps without a UTC offset are read as local time |
--fail-on LEVEL | lowest level that exits non-zero: warning (default), critical, or never. For CI gates |
--threshold FLOAT | minimum confidence to trust a classified category (default 0.35) |
--format NAME | force a parser instead of auto-detecting, e.g. --format nginx. Common aliases work: nginx, apache, k8s, dmesg, ssh, dnf, journal |
--no-baseline | skip comparison against historical baselines |
--json | machine-readable output instead of the terminal report (a list when several files are given) |
--config PATH | use a specific config file (see config) |
--verbose / -v | every evidence line at full width, plus debug detail on stderr |
--doctor | check the install (Python, classification model, state database) and exit |
--version / -V | print the installed version |
--install-completion | install shell completion |
--show-completion | print the completion script |
--help | full flag reference from the CLI itself |
$ logcrux /var/log/syslog # analyze a file
$ logcrux /var/log/ # a whole directory, worst file first
$ logcrux /var/log/syslog --last 1h # only the last hour
$ logcrux /var/log/syslog --format nginx # force a parser
$ logcrux /var/log/syslog --json # machine-readable output
$ logcrux app.log --fail-on critical # CI: fail only on CRITICAL
$ logcrux /var/log/syslog --verbose # all evidence, debug detail
$ logcrux --doctor # is the install healthy?
An incident, from a server's /var/log/secure:
$ logcrux /var/log/secure
╭─────────────────────── WARNING Auth Brute Force ────────────────────────╮
│ ● Authentication failure cluster (66 events) │
│ 2026-10-19 19:55:21 → 19:55:41 (20s) │
│ source: 203.0.113.45 ×59 · user: root ×4, admin ×4, oracle ×4 │
│ line 61: unix_chkpwd: password check failed for user (root) │
│ line 63: sshd-session: Failed password for root from 203.0.113.45 port… │
│ │
│ Confidence: 96% │
│ │
│ Remediation: Block offending IPs with `firewall-cmd --add-rich-rule` or │
│ fail2ban. Review /var/log/secure for source IPs and consider disabling │
│ password auth in sshd_config. │
╰──────────────────────────────────────────────────────────────────────────────╯
Checked 133 events as secure │ 100% of 133 lines parsed │ 2026-10-19
19:53:22 → 19:55:41 (2m19s) │ 44 error · 15 warning
Analyzed 133 events in 0.2s │ 1 incident(s) │ logcrux v0.10.0
Header: severity (WARNING / CRITICAL) and the
incident. Each finding: what it is, how many events, when it started and
ended, what it is about (here the attacking address and the accounts it
tried), and up to three log lines, by line number, that prove it. Then
the confidence and a suggested first action. Below the box, the
Checked line says what logcrux looked at: the format, how
much of the file it understood, the time span, the error and warning
counts.
A clean result says what was checked, so CLEAN never just means "didn't work":
╭────────────────────── CLEAN No incidents detected ──────────────────────╮
│ No error or warning lines; no crash, OOM, auth, disk or job failure found. │
╰──────────────────────────────────────────────────────────────────────────────╯
Checked 2 events as syslog │ 100% of 2 lines parsed │ 2026-10-20 10:00:01 →
10:00:02 (1s) │ 0 error · 0 warning
Analyzed 2 events in 0.0s │ 0 incident(s) │ logcrux v0.10.0
Empty input, a binary file, or journalctl's own "you can't see the system journal" notice are called out instead of reported as CLEAN. On a short log that stops on an error (what a container printed before it exited), that last line is shown on the CLEAN result.
$ logcrux /var/log/secure --json
Trimmed to the main keys:
{
"schema_version": "2",
"level": "WARNING",
"title": "Auth Brute Force",
"findings": [
{
"headline": "Authentication failure cluster",
"detail": "66 events",
"kind": "auth_failure_cluster",
"count": 66,
"first_seen": "2026-10-19T19:55:21",
"last_seen": "2026-10-19T19:55:41",
"subjects": [
{
"label": "source",
"value": "203.0.113.45",
"count": 59
},
{
"label": "user",
"value": "root",
"count": 4
},
{
"label": "user",
"value": "admin",
"count": 4
}
],
"evidence": [
{
"line": 61,
"timestamp": "2026-10-19T19:55:21",
"source": "unix_chkpwd",
"text": "Oct 19 19:55:21 web-01 unix_chkpwd[16964]: password check failed for user (root)",
"message": "password check failed for user (root)"
}
]
}
],
"confidence": 0.959,
"category": "auth_brute_force",
"remediation": "Block offending IPs with `firewall-cmd --add-rich-rule` or fail2ban. Review /var/log/secure for source IPs and consider disabling password auth in sshd_config.",
"log_path": "/var/log/secure",
"parser_format": "secure",
"parsed_count": 133,
"coverage": {
"lines": 133,
"parsed": 133,
"unparsed": 0,
"ratio": 1.0
},
"time_range": {
"start": "2026-10-19T19:53:22",
"end": "2026-10-19T19:55:41",
"duration_seconds": 139.0
}
}
schema_version is "2"; every key of the older
output keeps its meaning. Each finding carries its kind,
count, first_seen/last_seen,
subjects and evidence (line number, verbatim
text). coverage says how much of the input was understood,
warnings lists anything wrong with the input itself. A clean
run reports "level": "CLEAN" and "findings": [].
The full key reference is
in the repository.
| category | triggered by |
|---|---|
auth_brute_force | SSH, sudo, PAM, database, LDAP, SMB, MQTT, web-login failure clusters, in known and unknown wordings |
http_overload | 5xx floods, a proxy whose backends are all down |
network_issue | timeouts, refused connections, a rejected TLS certificate |
oom | the kernel OOM killer, Kubernetes OOMKilled, JVM and V8 out-of-memory |
service_crash | crashed units and backends, segfaults, an unhandled exception or panic that ended a process, failing health probes |
disk_full | "no space left on device" and its database-specific wordings |
config_error | a config file a service refused (nginx, Tomcat, BIND, Prometheus, Kafka), a Kubernetes image that cannot be pulled |
Some findings have their own name instead of one of these:
Job failed (a backup run that did not complete:
restic, kopia, borg; a package-manager run blocked by a lock),
Resource limit reached (file descriptors, processes,
workers, connections, cluster capacity), RAID array
degraded / Filesystem remounted read-only,
SELinux denied access, System clock jumped,
Mail rejected in bulk.
Script-friendly by design:
0 | clean, nothing found |
|---|---|
3 | info or warning-level finding |
4 | critical finding |
1 | bad option or config |
2 | input missing, unreadable, or binary |
With several files, the exit code is the worst verdict among them.
--fail-on critical exits non-zero only for CRITICAL;
--fail-on never exits 0 for any completed analysis.
Reading a file transparently decompresses it if it's gzip or bz2.
Detection is by magic number, not by trusting the
.gz/.bz2 extension, so a rotated
syslog.1.gz just works. A truncated or partially-corrupt
archive (say, a log rotated mid-write) is salvaged up to the point it
can still decompress, with a warning, rather than aborting outright.
Unparsed lines are counted in the Checked line, never dropped
without a trace. If the detected parser only understands part of a file,
the rest of the lines are still analyzed as generic text; if a stream
mixes formats (docker logs of nginx or Apache writes access
lines to stdout and errors to stderr), each line goes to the parser that
understands it. --format turns this off if you want to force
a specific parser.
docker logs and kubectl logs output can be
piped in as is: interleaved stdout and stderr, the
--timestamps column, kubectl logs --prefix,
ANSI colour codes and syslog <PRI> prefixes are all
handled.
Turn on the severity you want detected. Some services
only log failures at a raised verbosity (MySQL's
log_error_verbosity, Samba's log level, for
example). If the source never logs the event, there is nothing to find.
Read the log the failure is written to. Tomcat writes
servlet exceptions to logs/localhost.DATE.log, not to its
console; an OOM-killed container leaves nothing in its own output (pipe
kubectl describe pod or docker inspect instead).
Slice very large logs. The whole input is held in
memory while it is analyzed (roughly 1.3 KB per line). For multi-gigabyte
logs pipe a slice: tail -n 200000,
journalctl --since, or grep the service first.