logcrux / usage


usage

$ 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

how you invoke it

This page writes logcrux ... everywhere, which assumes the executable is on your PATH. Whether it is depends on how you installed it:

installed withinvoke as
uv tool install or pipxlogcrux ..., already on PATH
pip into a venv~/.venvs/logcrux/bin/logcrux ..., or activate the venv first
no install, one-off runuvx --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.

flags

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 DURATIONonly 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 LEVELlowest level that exits non-zero: warning (default), critical, or never. For CI gates
--threshold FLOATminimum confidence to trust a classified category (default 0.35)
--format NAMEforce a parser instead of auto-detecting, e.g. --format nginx. Common aliases work: nginx, apache, k8s, dmesg, ssh, dnf, journal
--no-baselineskip comparison against historical baselines
--jsonmachine-readable output instead of the terminal report (a list when several files are given)
--config PATHuse a specific config file (see config)
--verbose / -vevery evidence line at full width, plus debug detail on stderr
--doctorcheck the install (Python, classification model, state database) and exit
--version / -Vprint the installed version
--install-completioninstall shell completion
--show-completionprint the completion script
--helpfull flag reference from the CLI itself

examples

$ 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?

reading the report

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.

json output

$ 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.

incident categories

categorytriggered by
auth_brute_forceSSH, sudo, PAM, database, LDAP, SMB, MQTT, web-login failure clusters, in known and unknown wordings
http_overload5xx floods, a proxy whose backends are all down
network_issuetimeouts, refused connections, a rejected TLS certificate
oomthe kernel OOM killer, Kubernetes OOMKilled, JVM and V8 out-of-memory
service_crashcrashed 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_errora 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.

exit codes

Script-friendly by design:

0clean, nothing found
3info or warning-level finding
4critical finding
1bad option or config
2input 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.

rotated and compressed logs

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.

data loss is never silent

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.

feeding logs correctly

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.