Documentation

Everything you need to run microSRE on a single machine or across your fleet.

Contents

Installation

microSRE ships as a single Python zipapp. No third-party dependencies, no package manager; the one requirement is Python 3.11 or newer.

Root or a regular user? While a regular user is enough, running under root may be a better choice when:

curl -fsSL https://microsre.io/install.sh | bash

The installer downloads the latest release, creates a systemd service if systemd is present and starts it up.

To install manually, download microsre-<version>.pyz from the releases page, rename it to microsre, and make it executable. The rename is not cosmetic: whatever sits on your $PATH is the command you type, and nobody wants to type the versioned file name. Requires Python 3.11 or newer.

Verify

microsre --version

It will display locations of configuration file, logfile and unique machine identifier

Start, stop, restart

Root install - a system-wide service:

sudo systemctl restart microsre
sudo systemctl stop microsre
sudo systemctl start microsre

User install - a user-level service, no sudo:

systemctl --user restart microsre
systemctl --user stop microsre
systemctl --user start microsre

The service is enabled on install and starts automatically - at boot for a root install, at login for a user install. Check status with systemctl status microsre (add --user for a user install).


Quickstart

1. Run microSRE once to create the default configuration file:

microsre --version

As root the config is written to /etc/microsre/config.toml; as a regular user to ~/.config/microsre/config.toml. --version also prints the log file and machine ID locations.

2. Add a webhook to the config to enable alerts:

webhook_url = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"

3. Verify the configuration:

microsre --verify
# Configuration file valid

4. Watch metrics in the foreground:

microsre --snapshot

The dashboard prints every check - 60 seconds by default, change it with the interval setting - and shows which alerts would fire. Snapshot mode is a dry run - nothing is sent.

5. Start the daemon:

microsre --daemon

The daemon checks on the same interval and sends an alert the moment a threshold is crossed.


Command line

microsre [--snapshot [SECONDS]] [--daemon] [--verify] [--version]

Run without arguments, microsre creates the default config file if it is missing and exits.


Configuration

microSRE reads a single TOML file. The location depends on the user:

If the file does not exist, the built-in default is written there on first run with permissions 0600. There is no --config flag - edit the file in place.

Other files microSRE manages:

Full example

# Report to the microsre.io hosted view (optional)
# ingest_url = "https://eu.microsre.io/ingest?token=<your token>"

# Expose metrics for Prometheus scraping (optional)
# prometheus_listen = "127.0.0.1:9100"

# Seconds between checks in daemon mode
interval = 60

# Where alerts go (optional). Slack, Discord and Telegram webhook URLs are
# auto-detected from the URL; any other URL gets a plain JSON POST with a
# "text" field. No webhook_url - no chat alerts (they still go to the system log).
# webhook_url = "https://hooks.slack.com/services/..."
# or: webhook_url = "https://discord.com/api/webhooks/..."
# or: webhook_url = "https://api.telegram.org/bot<token>/sendMessage"
# username = "microsrebot"       # optional display name, Slack and Discord
# chat_id = "-1001234567890"     # Telegram only

[alerts]
# metric name                fire when value >= max for `duration` seconds
"mem.utilization_pct"    = { max = 90, duration = 0,   recovery_margin = 0.95, template = "metric" }
"cpu.utilization_pct"    = { max = 80, duration = 0,   recovery_margin = 0.95, template = "metric" }
"disk.space_usage_pct"   = { max = 90, duration = 0,   recovery_margin = 0.95, template = "metric" }
"oom.count"              = { max = 1,  duration = 0,   template = "oom" }
"logs.nginx.error_count" = { max = 10, duration = 120, recovery_margin = 0.95, template = "logs" }

[[logs]]
name = "nginx"
path = "/var/log/nginx/error.log"
error_pattern = "\\[error\\]|\\[crit\\]|\\[alert\\]|\\[emerg\\]"

[[docker-logs]]
# containers = ["django-app", "celery-worker"]   # default: all containers
error_pattern = ".(ERROR|CRITICAL|FATAL)."

[[templates]]
name = "metric"
title = "📊 {metric.name} alert on {hostname}"
body = """**{metric.name}**: {metric.value}
**Threshold**: {metric.min} {metric.max}
**Duration**: {metric.duration} seconds

Investigate: `microsre --snapshot` (live view), `vmstat 1` (runnable/blocked, swap), `cat /proc/pressure/*`"""

Top-level keys

Alert rules - [alerts]

Each key is a metric name, each value is a rule:

Alertable metrics

Every metric, exactly where it comes from, and how it is measured:

MetricSource fileSemantics
Memory
mem.utilization_pct/proc/meminfoinstantaneous
MemTotal, MemAvailable (used = total - available)
mem.swap_utilization_pct/proc/meminfoinstantaneous
SwapTotal, SwapFree
vm.swap_in_bytes_per_sec, vm.swap_out_bytes_per_sec, vm.major_faults_per_sec/proc/vmstatintegral (counter diff)
pswpin, pswpout, pgmajfault diffs over the interval, swap rates in bytes; the interval view of burst.vm.max_*
mem.pressure.{some,full}.avg{10,60} (+ .previous)/proc/pressure/memoryinstantaneous (kernel-windowed)
some / full line, avg10 / avg60
CPU
cpu.utilization_pct, cpu.iowait_pct, cpu.steal_pct/proc/statintegral
cpu line - user, nice, system, idle, iowait, irq, softirq, steal ticks, diffed over the interval
cpu.max_utilization_pct, cpu.max_utilization_core, cpu.<core>.utilization_pct/proc/statintegral
cpuN per-core lines
cpu.loadavg.1min (+ .previous)/proc/loadavginstantaneous (kernel-decayed)
1 min field
cpu.ctxt_per_sec/proc/statintegral (counter diff)
context switches per second, averaged over the interval; the interval view of burst.cpu.max_ctxt_per_sec
cpu.procs_blocked/proc/statinstantaneous
tasks in uninterruptible (D) sleep at tick time; the interval view of burst.cpu.max_procs_blocked
cpu.pressure.{some,full}.avg{10,60} (+ .previous)/proc/pressure/cpuinstantaneous (kernel-windowed)
some / full line, avg10 / avg60
Disk
disk.space_usage_pct/proc/self/mountinfoinstantaneous
per-mount statvfs bytes used / total; each device counted once
disk.max.utilization_pct, disk.max.await_ms, disk.max.queue_depth, disk.max.device/proc/diskstatsintegral; instantaneous (queue depth)
per device: time_in_progress diff; await = (time_reading + time_writing) / (reads + writes); queue depth = in_progress gauge; stacked dm/md deduplicated to the backing disk
disk.total.{read,write}_bytes_per_sec/proc/diskstatsintegral
sectors_read / sectors_written x 512-byte sectors (standard kernel units)
disk.total.{read,write}_iops/proc/diskstatscounter
reads / writes completed
disk.pressure.{some,full}.avg{10,60} (+ .previous)/proc/pressure/ioinstantaneous (kernel-windowed)
some / full line, avg10 / avg60
disk.<device>.*/proc/diskstatsintegral; instantaneous (queue depth)
per-device counters, diffs over the check interval
Network
net.total.{rx,tx}_bytes_per_sec/proc/net/devintegral
sum of per-interface rx_bytes / tx_bytes
net.total.errs_per_sec, net.total.drop_per_sec/proc/net/devcounter
rx_errs + tx_errs, rx_drop + tx_drop
net.tcp.retrans_per_sec, net.tcp.retrans_ratio_pct/proc/net/snmpcounter
Tcp: - RetransSegs, OutSegs
net.tcp.listen_drops, net.tcp.listen_overflows/proc/net/netstatcounter
TcpExt: - ListenDrops, ListenOverflows
net.max.utilization_pct, net.max.utilization_iface/proc/net/dev + /sys/class/net/<iface>/speedintegral
max of rx/tx bytes x8 as % of link speed (unknown for virtual or link-down interfaces)
net.max.{rx,tx}_bytes_per_sec, net.max.{rx,tx}_iface/proc/net/devintegral
busiest interface by bytes
net.<iface>.*/proc/net/devintegral; counter (errs, drop)
per-interface counters, diffs over the check interval
Burst
burst.cpu.max_utilization_pct, burst.cpu.max_utilization_core, burst.cpu.max_steal_pct, burst.cpu.max_iowait_pct, burst.cpu.max_ctxt_per_sec, burst.cpu.max_procs_running, burst.cpu.max_procs_blockedburst sampler: /proc/stat every secondmax over 1s sub-intervals
worst second in the window: hottest core, steal, iowait, context switches, runnable and blocked task counts; procs_running excludes the sampler itself
burst.disk.max_utilization_pct, burst.disk.max_utilization_deviceburst sampler: /proc/diskstats every secondmax over 1s sub-intervals
the busiest device-second in the window
burst.net.tcp.max_retrans_per_sec, burst.net.tcp.max_listen_drops_per_sec, burst.net.tcp.max_listen_overflows_per_secburst sampler: /proc/net/snmp, /proc/net/netstat every secondmax over 1s sub-intervals
worst second in the window for retransmits and listen queue drops
burst.vm.max_swap_in_bytes_per_sec, burst.vm.max_swap_out_bytes_per_sec, burst.vm.max_major_faults_per_secburst sampler: /proc/vmstat every secondmax over 1s sub-intervals
worst second in the window for swap in/out and major faults
burst.disk.<device>.*, burst.net.<iface>.*burst sampler: /proc/diskstats, /proc/net/dev every secondmax over 1s sub-intervals
per-device and per-interface worst-second values
burst.samples, burst.droppedburst samplercounter
how many sub-samples the window maxima rest on; dropped counts samples overwritten when the main loop fell behind
OOM kills
oom.count, oom.victimskernel log (auto-detected, no config)counter
OOM killer events
Log sources
logs.<source>.*, docker-logs.<source>.* (per configured log source)the configured log file or container JSON logscounter; gauge (heartbeat age)
line_count / error_count / error_pct / errors - lines matching line_pattern / error_pattern; with heartbeat_pattern configured: heartbeat_last_seen (unix time of last match, 0 = never), heartbeat_age_seconds (seconds since it, huge when never), heartbeat_status (pre-worded 'not seen yet' / 'not seen for N seconds' for templates)
Collection
collect.procfs_seconds, collect.proc_pid_secondsself-measured, time.monotonic_ns() around the collectorsmeasured duration
procfs collection time per tick; proc_pid is the /proc/<pid> scan portion of it
collect.logs_seconds, collect.docker_logs_seconds, collect.oom_secondsself-measured, time.monotonic_ns() around the collectorsmeasured duration
log scanning time per tick
self.cpu_seconds, self.cpu_pct, self.rss_bytes, self.read_bytes_per_sec, self.ctx_switches_per_secself-measured, /proc/self/stat, /proc/self/io, /proc/self/statusmixed: counter, integral, instantaneous
microsre itself: total CPU seconds, CPU pct last interval, RSS, storage reads caused, wakeups per second

Caveats

hostname, timestamp, and uptime (from /proc/uptime) are also available - mostly useful in templates.

Templates - [[templates]]

Alert messages are rendered with Python format-string placeholders:

Placeholders that don't match any known field are reported by microsre --verify.

Prometheus

Optional. Set prometheus_listen and the daemon serves the metrics listed above in Prometheus text format at /metrics - point a scrape target at it. Without this setting nothing listens. The endpoint has no authentication: localhost is the safe default, and exposing it to a network should be a deliberate choice (firewall or reverse proxy).

Exposed names carry a microsre_ prefix with dots turned into underscores - mem.utilization_pct becomes microsre_mem_utilization_pct. Per-core, per-device, and per-interface families become labels instead: microsre_cpu_utilization_per_core{core="cpu0"}, microsre_disk_await_ms{device="sda"}, microsre_net_rx_bytes_per_sec{iface="eth0"}. Everything is a gauge. Non-numeric fields (hostnames, error text, the max-util summaries' device names) are not exported.


System Metrics

microSRE reads /proc directly. No shelling out to ps, top, or iostat.

CPU

Source: /proc/stat

CPU usage is calculated by diffing user, system, and idle ticks between intervals. The result is a percentage of total capacity.

Memory

Source: /proc/meminfo

Used memory is MemTotal - MemAvailable. This matches what the OOM killer sees, not what free reports.

Disk I/O

Source: /proc/diskstats

Reads and writes are counted per block device, along with utilization, await, and queue depth. When device-mapper or md devices stack on top of real disks (LVM, dm-crypt, RAID), only the backing disk is counted so totals are not double-counted. Space usage comes from statvfs on each physical mount.

Network

Source: /proc/net/dev

Bytes and packets in/out are tracked per interface. Dropped and error counters are included in alerts.

Pressure

Source: /proc/pressure/cpu, /proc/pressure/memory, /proc/pressure/io

Pressure stall information (PSI) is available on kernels 4.20+. It measures how much time tasks spend waiting for resources. This is the same signal systemd uses for OOM avoidance.


Log Monitoring

microSRE tails log files and counts lines. Each [[logs]] entry takes a name and an absolute path:

[[logs]]
name = "nginx"
path = "/var/log/nginx/error.log"
line_pattern = ".*"                       # default: every line counts
error_pattern = ".*"                      # default: every line is an error

On every check, each source reports logs.<name>.line_count, .error_count, .error_pct, and .errors (the recent matching lines). Alert on any of them - e.g. max = 10 on logs.nginx.error_count fires when more than 10 error lines appear in a single check.

Example: Nginx errors

[[logs]]
name = "nginx"
path = "/var/log/nginx/error.log"
error_pattern = "\\[error\\]|\\[crit\\]|\\[alert\\]|\\[emerg\\]"
"logs.nginx.error_count" = { max = 10, duration = 120, template = "logs" }

This fires when at least 10 error-level lines appear per check and the total stays above the threshold for 120 seconds, and uses the logs template which includes the recent error lines.

Heartbeats: alert on absence

Some jobs only need to prove they ran. Set heartbeat_pattern to the line a healthy source logs regularly, and microSRE reports logs.<name>.heartbeat_last_seen (unix time of the last match, 0 = never), logs.<name>.heartbeat_age_seconds (seconds since it), and logs.<name>.heartbeat_status (a pre-worded "not seen yet" / "not seen for N seconds" for templates):

[[logs]]
name = "nightly-batch"
path = "/var/log/nightly-batch.log"
heartbeat_pattern = "batch (done|finished)"
"logs.nightly-batch.heartbeat_age_seconds" = { max = 87000, duration = 0, template = "heartbeat" }

This fires when nothing matched the pattern for 24 hours plus 20 minutes of drift. Until the first match the age is huge, so a job that never runs at all alerts right away. The last-seen time is persisted in state.sqlite and survives restarts; an edited heartbeat_pattern starts over, so a typo alerts within one interval instead of up to a day later. A beat that arrived during a downtime longer than ten minutes is missed and may alert once - it self-heals at the next beat. It works for [[docker-logs]] sources as well.

Docker logs

[[docker-logs]]
# containers = ["django-app", "celery-worker"]   # default: all containers
line_pattern = "^\\d{4}-\\d{2}-\\d{2}"   # e.g. lines starting with a timestamp
error_pattern = ".(ERROR|CRITICAL|FATAL)."

When /var/lib/docker/containers is readable by the current user (root on a standard install, or the owner under rootless Docker), microSRE reads the container JSON logs directly and keeps file offsets - cheap. Otherwise it falls back to docker logs --since on every check, which needs the docker socket (root or the docker group) and becomes slow and IO-intensive unless log rotation is set up. Docker Compose replica suffixes (-1, _1) are stripped, so containers lists service names. Metrics are reported per container as docker-logs.<name>.*. A stopped or removed container keeps reporting a tick with zero lines, so heartbeat ages keep growing and the dead man's switch still fires. In this direct-file mode, read positions are kept in state.sqlite: a restart within ten minutes of downtime picks up where it left off (heartbeat sources catch up on what they missed; other sources resume at the end of the log).

Multiple log sources

Add as many [[logs]] and [[docker-logs]] sections as you need. Each is independent.

Permissions

microSRE must have read access to the log files. If running as a non-root user, add that user to the adm or systemd-journal group, or adjust file ACLs.


Alerting

Alerts go straight from your machine to a single webhook_url - Slack, Discord, Telegram, or any endpoint that accepts a JSON POST. The hosted service is not involved in delivery. Omit the key to run without chat alerts; alert lines still land in the system log.

# Slack
webhook_url = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXX"
username = "microsrebot"      # optional

# Discord
webhook_url = "https://discord.com/api/webhooks/..."

# Telegram - chat_id required
webhook_url = "https://api.telegram.org/bot123456:ABC-DEF.../sendMessage"
chat_id = "-1001234567890"

For Telegram private chats, the chat ID is your user ID. For groups, add the bot to the group and use the group ID (usually a negative number). If Telegram returns HTTP 400, the bot is usually missing channel admin rights.

How alerts fire

Real world

Telegram messages from microsre: a disk.space_usage_pct alert on host blackhole at 19:39 and a Recovered message seven minutes later
This is a real alert, from a real machine, on a real Saturday - delivered by the free local agent, straight from the server to Telegram. A scraping server hit 90% disk at 19:39; seven minutes of deleting old scrape data later, "Recovered."

Choosing between monitoring tools? There is an honest map of the options - Netdata, Prometheus, healthchecks.io, Uptime Kuma, Monit - including when microSRE is the wrong tool.


Fleet / SaaS

This is your command and control center with health checks from all your machines in one place

Connecting a machine

1. Sign up at microsre.io to get an ingest URL with a token.

2. Add one line to the top of your config:

ingest_url = "https://eu.microsre.io/ingest?token=<your token>"

3. Restart the daemon. The machine appears in your fleet view within one check, reports a heartbeat every check, and sends "up" / "down" events on start and shutdown. If a machine stops checking in, the hosted view alerts - a dead man's switch, not a missing data point.


Troubleshooting

No alerts received

High CPU from microSRE itself

Permission denied on /proc

Logs not matching

Systemd service fails to start

journalctl -u microsre -n 50 --no-pager

Common causes: invalid TOML syntax, a bad regex in a log pattern, a missing Telegram chat_id, or missing read permissions on log files. The config file itself is created automatically on first run - check the path printed by microsre --version.


Still stuck? Open an issue on GitHub or email hello@microsre.io.