One bash file watches your logs, your host, your file integrity and your kernel, then tells you on Discord when something changes. No agent fleet, no collector, no retention tier.
Written for people who run one server, or three, and read the logs themselves. This page is also the command reference.
curl -fsSL https://raw.githubusercontent.com/chud-lori/milog/main/install.sh | sudo bash
Prefer to read it first? install.sh is plain bash. It runs on Linux only and exits on any other OS before installing anything.
Six things it watches, one alert pipeline
Everything below fires through one alert pipeline, with the same per-rule cooldown, silences, routing and hooks. Request-log rules also share a dedup keyed on (ip, path), so a line that matches two rules alerts once. Turning on a new surface does not mean a new place to configure alerts.
nginx combined format gets the full treatment. Other sources are tailed, grepped and searched, and skip the parsing modes.
Read straight from the kernel, so nothing extra has to be installed.
Pattern matching over request lines, tuned for what actually hits a small box.
Ranked by behaviour, not just by name, so a renamed agent still surfaces.
Baseline once while the box is known good, then diff. Needs no web server at all, so it works on a host that serves nothing.
An optional Go binary. Catches the short-lived process that a log tail never sees.
exec: a shell under a web worker, a binary run from /tmptcp connects to destinations outside the allowlisted CIDRs, except milog's own alert sendsfile opens of shadow, sudoers and SSH config by processes outside the allowlistptrace, kmod, bpf-loadEvery command
The same list milog help prints. Any command takes
--help for its own detail. Before any log source is
configured, only help, config, doctor, version, update, install,
completions, alert, alerts, silence, audit, probe and bench run; the rest exit and point at
milog config init.
Nothing matches that.
Point it at your logs
A bare name means an nginx access log in LOG_DIR. A prefix
means something else, which is how MiLog watches an app that never touches
nginx.
# ~/.config/milog/config.sh
LOGS=(
api # $LOG_DIR/api.access.log
nginx:storefront # the same thing, said explicitly
text:worker:/var/log/worker/out.log # any text file
journal:mybot.service # streams via journalctl
docker:postgres-prod # the container's stdout
)
| Source | Resolves to | Supports |
|---|---|---|
| api | $LOG_DIR/api.access.log | everything |
| nginx:api | the same file, explicitly | everything |
| text:name:/path | that file | tail, grep, search, errors, patterns |
| journal:unit | journalctl -u unit -f | tail, grep, search, errors, patterns |
| docker:name | the container JSON log | tail, grep, search, errors, patterns |
The counting modes need the nginx combined format to read a status code and a latency, so they stay with nginx sources. Host integrity audits and the kernel probe read no logs at all and work on any host.
These environment variables win over the config file, so one run can
differ without editing anything. MILOG_APPS replaces
LOGS.
MILOG_CONFIG MILOG_LOG_DIR MILOG_REFRESH
MILOG_ALERTS_ENABLED MILOG_ALERT_ROUTES MILOG_ALERT_COOLDOWN
MILOG_ALERT_DEDUP_WINDOW MILOG_ALERT_HOOK_TIMEOUT MILOG_ALERT_LOG_MAX_BYTES
MILOG_DISCORD_WEBHOOK MILOG_SLACK_WEBHOOK MILOG_TELEGRAM_BOT_TOKEN
MILOG_TELEGRAM_CHAT_ID MILOG_MATRIX_HOMESERVER MILOG_MATRIX_ROOM
MILOG_MATRIX_TOKEN MILOG_WEBHOOK_URL MILOG_WEBHOOK_TEMPLATE
MILOG_WEBHOOK_CONTENT_TYPE MILOG_HOOKS_DIR
MILOG_APPS MILOG_AUDIT_ENABLED MILOG_AUDIT_FIM_INTERVAL
MILOG_AUDIT_PERSISTENCE_INTERVAL MILOG_AUDIT_PORTS_INTERVAL
MILOG_AUDIT_YARA_INTERVAL MILOG_AUDIT_YARA_PATHS MILOG_AUDIT_YARA_RULES_DIR
MILOG_AUDIT_ACCOUNTS_INTERVAL MILOG_AUDIT_ACCOUNTS_PATHS
MILOG_AUDIT_ROOTKIT_INTERVAL
MILOG_HISTORY_ENABLED MILOG_HISTORY_DB MILOG_ANOMALY_ENABLED
MILOG_ANOMALY_SIGMA MILOG_ANOMALY_MIN_DAYS MILOG_ANOMALY_FLOOR_REQ
MILOG_ANOMALY_FLOOR_C5XX MILOG_ANOMALY_FLOOR_P95
MILOG_PATTERNS_ENABLED MILOG_SLOW_EXCLUDE_PATHS
MILOG_GEOIP_ENABLED MILOG_MMDB_PATH MILOG_DOCKER_ROOT
MILOG_CROWDSEC_CTI_KEY
MILOG_WEB_PORT MILOG_WEB_BIND
Each one is described in docs/configuration.md.
Getting it running
Step 1 is the whole install. The rest are the things worth turning on afterwards, in the order they pay off.
curl -fsSL https://raw.githubusercontent.com/chud-lori/milog/main/install.sh | sudo bash
Linux only. Drops milog into /usr/local/bin, pulls gawk,
curl and sqlite3 through apt, dnf, yum, pacman or apk, and stops with
an error on a host with none of those. The milog-web,
milog-tui and milog-probe binaries come from
the latest release, and each archive is checked against that
release's checksums.txt (SHA-256); a mismatch aborts the
install. checksums.txt is not signed, so this catches a
corrupted or swapped download, not a compromised release.
milog.sh itself is fetched from the main branch and is
not checksummed.
The release page
also has .deb, .rpm and .apk
packages for amd64 and arm64.
milog doctor # what is missing before anything else
milog monitor # the dashboard
Run doctor first. It checks the log format, which is the
one thing that quietly halves what MiLog can tell you.
sudo milog alert on 'https://discord.com/api/webhooks/ID/TOKEN'
milog alert test
alert on writes the Discord webhook to your config and
installs milog.service, which runs as the user who ran
sudo. alert test sends one message to every configured
destination. Slack, Telegram, Matrix and a generic webhook are set
with milog config set, described in
docs/alerts.md;
alert on still wants a Discord webhook before it installs
the service.
milog audit fim baseline
milog audit ports baseline
milog audit accounts baseline
Drift detection compares against a known-good snapshot, so a baseline taken after a compromise records the compromise as normal. This is the step worth doing today rather than eventually.
milog config set HISTORY_ENABLED 1
sudo systemctl restart milog.service
# two weeks later
milog config set ANOMALY_ENABLED 1
milog auto-tune 7
The anomaly detector compares each minute against the same minute over the previous days, so it needs days before it says anything. It stays quiet until it has them.
sudo milog probe install-service
sudo journalctl -u milog-probe.service -f
Catches what a log tail structurally cannot: the process that started and exited between two reads. Same webhooks, same cooldown. The unit runs the probe as root, limited by a systemd capability bounding set, and the alerts it raises run as the user who ran sudo, through that user's config. Details in docs/probe.md.
Longer form
Each guide opens with what the thing is, then why you would turn it on, then how, with the failure modes named.
log_format recipe
Alerts
Discord, Slack, Telegram and Matrix setup, the rule catalog, fire history
Host integrity audits
FIM, rootkit, persistence, ports, YARA, accounts
Kernel observability
The eBPF sidecar, its eight probes, allowlist tuning
milog daemon
Headless mode, the systemd unit, permissions, the decision log
Historical metrics
SQLite storage, trend, diff, auto-tune, the sigma detector
Web dashboard
milog web, and how to reach it over SSH, Tailscale or Cloudflare
TUI
The Charm binary, key help, scrollable focused views
GeoIP
MaxMind licence, weekly refresh, the country column
Troubleshooting
milog doctor and the failure modes it names
How it is built, and why
milog.sh built from modular src/*.sh.
You install one file, contributors edit small ones.
alerts.log before any destination
is tried, and milog alerts reads it back. A delivery that
fails goes to send_failures.log, and
milog doctor reports the last 24 hours of them.