Skip to main content

logometer

Tail your logs. Catch anomalies. Skip the 2am grep.

logometer watches a log file (or stdin), buckets lines into time windows, and flags windows that look statistically off — an error spike, or an error type that's never shown up before. It's silent when things are fine. No AI required for the core detection; point it at an LLM with --explain if you want a plain-English guess at why a window looks weird.

Zero dependencies for the core tool — just Python 3.10+.

Install

As a standalone command, without cloning:

pipx install git+https://github.com/AmanSg098/logometer.git

Or from a clone, for development:

git clone https://github.com/AmanSg098/logometer.git
cd logometer
pip install -e .              # core tool, no dependencies
pip install -e ".[pretty]"    # optional: styled output via rich

Quick start

Try it on the included sample log (has a normal-traffic baseline plus an injected error burst):

python3 -m logometer.cli tail examples/sample.log --replay

or, once installed:

logometer tail examples/sample.log --replay

Usage

# Tail a live log file (like tail -f, but with anomaly detection)
logometer tail /var/log/app.log

# Pipe from stdin
tail -f app.log | logometer tail -

# Replay a static file from start to finish, then exit
logometer tail app.log --replay

# Only show anomalous windows, hide the "ok" noise
logometer tail app.log --replay --quiet

# Adjust window size (seconds) and how aggressively to flag deviations
logometer tail app.log --window 30 --sensitivity high

# Mute known-noisy lines so they never reach the baseline (repeatable regex)
logometer tail app.log --ignore 'healthcheck' --ignore 'DeprecationWarning'

# Ask an LLM for a one-sentence explanation of each anomaly
export ANTHROPIC_API_KEY=your-key-here
logometer tail app.log --explain

# JSON output, one object per line — good for piping into other tools
logometer tail app.log --format json

Stop a live tail with Ctrl-C (or SIGTERM from a process manager); the window in progress is reported before exiting.

All options

Option Default What it does
file — Log file to read, or - for stdin
--window SECONDS 10 Window size, when timestamps are parseable
--sensitivity medium low, medium or high — how far above the baseline a window must be to count as a spike
--format plain plain or json
--replay off Read the file start to end and exit, instead of following it live
--quiet off Only print anomalous windows
--ignore REGEX — Drop matching lines before analysis; repeatable
--explain off Ask an LLM for a one-sentence cause of each anomaly
--explain-provider anthropic anthropic, openai or openrouter
--explain-model MODEL see below Override the model used by --explain
--no-pretty off Use plain output even if rich is installed
--version — Print the version and exit

Supported timestamp formats

Timestamps are read from each line to decide which window it belongs to. Two formats are recognised:

  • ISO 8601 — 2026-09-17T12:03:10, 2026-09-17 12:03:10.123Z, and Python logging's default 2026-09-17 12:03:10,123
  • Syslog — Sep 17 12:03:10 (syslog has no year, so the current year is assumed)

The first 20 lines decide the mode: if at least half have a recognisable timestamp, lines are bucketed by time; otherwise every 50 lines form a window.

--explain

Requires an API key for one of three providers, set in your environment:

--explain-provider Key Default model
anthropic (default) ANTHROPIC_API_KEY claude-haiku-4-5-20251001
openai OPENAI_API_KEY gpt-4o-mini
openrouter OPENROUTER_API_KEY anthropic/claude-haiku-4.5

OpenRouter gives one key access to models from many vendors; its model ids are prefixed with the vendor (openai/gpt-4o-mini, google/gemini-2.5-flash, …). No SDK install needed — it's a plain HTTPS call. If the key is missing or the request fails, logometer prints a one-line warning to stderr and keeps tailing normally; it never crashes because --explain had a bad day.

Only the anomalous window's ERROR and WARN lines are sent (capped at 30 lines). Pick a different model with --explain-model:

export OPENAI_API_KEY=your-key-here
logometer tail app.log --explain --explain-provider openai --explain-model gpt-4o

export OPENROUTER_API_KEY=your-key-here
logometer tail app.log --explain --explain-provider openrouter --explain-model google/gemini-2.5-flash

--ignore

Real logs usually have one or two messages that fire constantly and mean nothing. Left alone they dominate the error count, so the rolling baseline learns them as "normal" and a genuine problem has to shout louder to get noticed. --ignore drops matching lines before any analysis:

# on a real macOS install log, one benign repeating message accounted for
# 97% of all "errors" — muting it cut the noise dramatically
logometer tail /var/log/install.log --replay --window 60 --quiet \
  --ignore 'installation-check'          # 27 anomalies -> 5, all genuine

Each --ignore takes a regular expression and can be repeated. An invalid pattern is reported as a normal CLI error rather than a crash.

Live tailing

logometer tail app.log (without --replay) follows the file the way tail -f does, with two things a naive tail loop gets wrong:

  • Windows close on time. A window is emitted once its duration has elapsed, even if no further lines arrive. Without this, a service that errors and then dies would never report that final burst — the most important one — because nothing follows it to trigger the flush.
  • Rotation is handled. If the log is rotated out from under it (logrotate, or truncated in place with copytruncate), it notices and follows the new file instead of reading a now-orphaned file forever.

Output is flushed as each window is reported, so piping or redirecting works in real time:

logometer tail app.log --quiet --format json >> alerts.jsonl
tail -f app.log | logometer tail -

Pretty output

If you pip install rich (or pip install -e ".[pretty]"), terminal output automatically upgrades to styled panels. Not required — plain ANSI output works everywhere. When output is piped or redirected to a file it stays plain text either way; pass --no-pretty to get plain output in the terminal too.

Example output

From the included sample log:

$ logometer tail examples/sample.log --replay --no-pretty

  [12:01:20 – 12:01:30]  ok        errors: 0   baseline: ~0.1

  [12:01:30 – 12:01:40]  ! ANOMALY  errors: 10  warns: 1  baseline: ~0.1   score: 9.9x
    New error signature detected: '<x> ERROR ConnectionResetError: [Errno <x>] Connection reset by peer id=<x>'

  [12:01:40 – 12:01:50]  ! ANOMALY  errors: 6  baseline: ~0.1   score: 5.9x
    (error rate spike — no brand-new error signature)

  [12:01:50 – 12:02:00]  ok        errors: 0   baseline: ~0.1
  ...
-- 18 window(s) processed, 4 anomaly(ies) flagged --

score is how many standard deviations the window's error count sits above the baseline. <x> marks the ids, numbers and timestamps stripped out when building an error signature.

With --explain, each anomaly gets one more line:

  [12:01:30 – 12:01:40]  ! ANOMALY  errors: 10  warns: 1  baseline: ~0.1   score: 9.9x
    New error signature detected: '<x> ERROR ConnectionResetError: [Errno <x>] Connection reset by peer id=<x>'
    Explanation: Database or downstream service became unresponsive, causing clients to forcibly close connections due to timeouts or hangs.

With --format json, each window is one line with these fields:

{"window_index": 9, "start": "12:01:30", "end": "12:01:40", "error_count": 10, "warn_count": 1, "baseline_mean": 0.111, "error_score": 9.889, "is_anomaly": true, "new_shapes": ["<x> ERROR ConnectionResetError: [Errno <x>] Connection reset by peer id=<x>"], "explanation": null}

The end-of-run summary line is omitted in JSON mode, so every line of output is valid JSON.

How the detection works

How logometer processes a log: prepare each line, judge each window, report

  1. Classify. Each line is tagged by keyword: ERROR (error, err, fatal, critical, exception, traceback, panic), then WARN (warn, warning), INFO (info, notice), DEBUG (debug, trace). First match wins, so a line mentioning both an error and a warning counts as ERROR.
  2. Fingerprint. ERROR and WARN lines are reduced to a "shape": UUIDs, hex addresses, timestamps, quoted strings and numbers are replaced with <x>, so the same underlying error collapses to one shape regardless of the specific id.
  3. Window. Lines are batched into fixed-size windows — by time if timestamps are parseable, otherwise by a fixed line count.
  4. Baseline. The error counts of the last 20 windows give a rolling mean and standard deviation — what "normal" error volume looks like. Scoring starts once 3 windows of history exist.
  5. Flag. A window is anomalous if either:
    • its error count is at least N standard deviations above the mean, where N is 3.0 for --sensitivity low, 2.0 for medium and 1.2 for high; or
    • it contains an error or warning shape not seen earlier in this run (checked from the second window on).

Two tuning choices keep this honest:

  • Noise floor. The standard deviation is never taken as less than 1.0. On a log that's normally error-free the real deviation is 0, and a single stray error would otherwise score as infinitely anomalous.
  • Spikes don't train the baseline. A window flagged as a spike isn't added to the history, so a long outage doesn't slowly teach the tool that a high error rate is normal.

No ML model, no training step — deliberately simple and explainable.

Known limitations

Worth knowing before you point this at something you care about. These are real, tested behaviours, not hypotheticals:

  • Severity is keyword matching, not parsing. A line counts as an error if it contains a word like error, fatal or critical. That means Downloading 1 products: Critical [] — an empty list, i.e. good news — is counted as an error. Use --ignore to mute these.
  • Python tracebacks are only partly seen. Traceback (most recent call last): is detected, but the final ValueError: ... line is not, because the match looks for error as a standalone word. And since that first line is identical for every exception, new-signature detection can't tell one crash type from another. Logs that print their own level (ERROR ValueError: ...) work properly.
  • JSON logs break new-signature detection. Fingerprinting strips quoted strings, which in a JSON line removes the entire message — so unrelated errors collapse into one shape. Error counting still works. Extracting the message first works around it: jq -r '.level + " " + .msg' app.log | logometer tail -.
  • Only the error count is scored. A flood of identical warnings won't trigger a spike (though a never-seen-before warning shape will be flagged), and a sudden drop in traffic isn't detected either — only rises above the baseline are.
  • Window labels show time, not date. On a log spanning several days you'll see the same [17:14:39 – 17:15:39] label more than once.
  • Timezone offsets are ignored. 2026-09-20 19:05:04+05:30 is read as local wall-clock time; a log mixing offsets is bucketed as though they were the same clock.
  • Nothing persists between runs. The baseline and the set of known error shapes are rebuilt from scratch each start, so expect the first few windows of any run to over-flag. (Persistence is on the roadmap.)
  • If timestamps can't be parsed it silently falls back to fixed 50-line windows, and --window stops having any effect. You can tell from the labels: [line 1 – line 50] instead of a time range.
  • --explain only sees ERROR and WARN lines. Lines without a level keyword — like the body of a Python traceback — aren't sent, so the model can miss the root cause and give a vaguer answer.
  • --explain sends log lines to a third party. There's no redaction — don't use it on logs containing secrets or personal data.

Running the tests

python3 -m unittest discover -s tests -v

No network or API key is needed — the --explain tests mock the HTTP call.

Project layout

logometer/
  cli.py         command-line entry point, file/stdin reading, output formatting
  classifier.py  severity tagging and error-shape fingerprinting
  timeparse.py   timestamp extraction (ISO 8601, syslog)
  windower.py    groups lines into time- or count-based windows
  baseline.py    rolling mean / standard deviation of errors per window
  detector.py    decides whether a window is anomalous
  explain.py     optional LLM explanations (Anthropic / OpenAI / OpenRouter over plain HTTPS)
  pretty.py      optional rich-styled output
tests/           unit and end-to-end tests
examples/        sample log with an injected error burst
docs/            flow diagram (Mermaid source + rendered PNG)

Roadmap

  • Config file for custom log-format parsing
  • Multiple file / glob support
  • Slack/Discord webhook alerts
  • Persistent anomaly history (SQLite)

Contributing

Issues and PRs welcome — especially around log-format parsing (every stack logs differently) and reducing false positives.

License

MIT — see LICENSE.

Release files for logometer 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for logometer 0.1.0
File Size Uploaded
logometer-0.1.0.tar.gz 35.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for logometer 0.1.0
File Interpreter ABI Platform
logometer-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 61.9 kB

Release files / logometer-0.1.0.tar.gz

Download URL logometer-0.1.0.tar.gz
Size 35.3 kB
Tags Source
SHA-256 checksum
How to use checksums
e29ef90aa4f367d0135662a73ae616775489b6d666093d2ee4a35f1c0c3cb7fa
BLAKE2b-256 checksum
How to use checksums
c7fce6733c02fae8b678f7deff7e4255d5b8544f0177d686c3495f897c081793
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Zorin OS","version":"18","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / logometer-0.1.0-py3-none-any.whl

Download URL logometer-0.1.0-py3-none-any.whl
Size 26.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
275082ee005140e61ef81efb684c244726b3272d08e0e07fae9b4262751464be
BLAKE2b-256 checksum
How to use checksums
712ed745e1eb786e242d2c954c7600c345be6d848ab34af18a5f4edd9328c907
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Zorin OS","version":"18","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page