logometer 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- logometer-0.1.0/LICENSE +21 -0
- logometer-0.1.0/PKG-INFO +294 -0
- logometer-0.1.0/README.md +265 -0
- logometer-0.1.0/logometer/__init__.py +3 -0
- logometer-0.1.0/logometer/baseline.py +54 -0
- logometer-0.1.0/logometer/classifier.py +71 -0
- logometer-0.1.0/logometer/cli.py +447 -0
- logometer-0.1.0/logometer/detector.py +70 -0
- logometer-0.1.0/logometer/explain.py +185 -0
- logometer-0.1.0/logometer/pretty.py +52 -0
- logometer-0.1.0/logometer/timeparse.py +58 -0
- logometer-0.1.0/logometer/windower.py +247 -0
- logometer-0.1.0/logometer.egg-info/PKG-INFO +294 -0
- logometer-0.1.0/logometer.egg-info/SOURCES.txt +21 -0
- logometer-0.1.0/logometer.egg-info/dependency_links.txt +1 -0
- logometer-0.1.0/logometer.egg-info/entry_points.txt +2 -0
- logometer-0.1.0/logometer.egg-info/requires.txt +3 -0
- logometer-0.1.0/logometer.egg-info/top_level.txt +1 -0
- logometer-0.1.0/pyproject.toml +43 -0
- logometer-0.1.0/setup.cfg +4 -0
- logometer-0.1.0/tests/test_explain.py +148 -0
- logometer-0.1.0/tests/test_live.py +263 -0
- logometer-0.1.0/tests/test_smoke.py +165 -0
logometer-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AmanSg098
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
logometer-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: logometer
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Tail your logs. Catch anomalies. Skip the 2am grep.
|
|
5
|
+
Author: AmanSg098
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/AmanSg098/logometer
|
|
8
|
+
Project-URL: Issues, https://github.com/AmanSg098/logometer/issues
|
|
9
|
+
Keywords: logs,logging,anomaly-detection,monitoring,cli,tail
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: System Administrators
|
|
14
|
+
Classifier: Operating System :: POSIX
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: System :: Logging
|
|
21
|
+
Classifier: Topic :: System :: Monitoring
|
|
22
|
+
Classifier: Topic :: Utilities
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
License-File: LICENSE
|
|
26
|
+
Provides-Extra: pretty
|
|
27
|
+
Requires-Dist: rich>=13; extra == "pretty"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# logometer
|
|
31
|
+
|
|
32
|
+
**Tail your logs. Catch anomalies. Skip the 2am grep.**
|
|
33
|
+
|
|
34
|
+
`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.
|
|
35
|
+
|
|
36
|
+
Zero dependencies for the core tool — just Python 3.10+.
|
|
37
|
+
|
|
38
|
+
<!-- TODO: record a short asciinema/GIF demo and drop it here before publishing.
|
|
39
|
+
Suggested recording: `logometer tail examples/sample.log --replay` -->
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
As a standalone command, without cloning:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pipx install git+https://github.com/AmanSg098/logometer.git
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or from a clone, for development:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
git clone https://github.com/AmanSg098/logometer.git
|
|
53
|
+
cd logometer
|
|
54
|
+
pip install -e . # core tool, no dependencies
|
|
55
|
+
pip install -e ".[pretty]" # optional: styled output via rich
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
Try it on the included sample log (has a normal-traffic baseline plus an injected error burst):
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
python3 -m logometer.cli tail examples/sample.log --replay
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
or, once installed:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
logometer tail examples/sample.log --replay
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Usage
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Tail a live log file (like tail -f, but with anomaly detection)
|
|
76
|
+
logometer tail /var/log/app.log
|
|
77
|
+
|
|
78
|
+
# Pipe from stdin
|
|
79
|
+
tail -f app.log | logometer tail -
|
|
80
|
+
|
|
81
|
+
# Replay a static file from start to finish, then exit
|
|
82
|
+
logometer tail app.log --replay
|
|
83
|
+
|
|
84
|
+
# Only show anomalous windows, hide the "ok" noise
|
|
85
|
+
logometer tail app.log --replay --quiet
|
|
86
|
+
|
|
87
|
+
# Adjust window size (seconds) and how aggressively to flag deviations
|
|
88
|
+
logometer tail app.log --window 30 --sensitivity high
|
|
89
|
+
|
|
90
|
+
# Mute known-noisy lines so they never reach the baseline (repeatable regex)
|
|
91
|
+
logometer tail app.log --ignore 'healthcheck' --ignore 'DeprecationWarning'
|
|
92
|
+
|
|
93
|
+
# Ask an LLM for a one-sentence explanation of each anomaly
|
|
94
|
+
export ANTHROPIC_API_KEY=your-key-here
|
|
95
|
+
logometer tail app.log --explain
|
|
96
|
+
|
|
97
|
+
# JSON output, one object per line — good for piping into other tools
|
|
98
|
+
logometer tail app.log --format json
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Stop a live tail with Ctrl-C (or `SIGTERM` from a process manager); the window in progress is reported before exiting.
|
|
102
|
+
|
|
103
|
+
### All options
|
|
104
|
+
|
|
105
|
+
| Option | Default | What it does |
|
|
106
|
+
|---|---|---|
|
|
107
|
+
| `file` | — | Log file to read, or `-` for stdin |
|
|
108
|
+
| `--window SECONDS` | `10` | Window size, when timestamps are parseable |
|
|
109
|
+
| `--sensitivity` | `medium` | `low`, `medium` or `high` — how far above the baseline a window must be to count as a spike |
|
|
110
|
+
| `--format` | `plain` | `plain` or `json` |
|
|
111
|
+
| `--replay` | off | Read the file start to end and exit, instead of following it live |
|
|
112
|
+
| `--quiet` | off | Only print anomalous windows |
|
|
113
|
+
| `--ignore REGEX` | — | Drop matching lines before analysis; repeatable |
|
|
114
|
+
| `--explain` | off | Ask an LLM for a one-sentence cause of each anomaly |
|
|
115
|
+
| `--explain-provider` | `anthropic` | `anthropic`, `openai` or `openrouter` |
|
|
116
|
+
| `--explain-model MODEL` | see below | Override the model used by `--explain` |
|
|
117
|
+
| `--no-pretty` | off | Use plain output even if `rich` is installed |
|
|
118
|
+
| `--version` | — | Print the version and exit |
|
|
119
|
+
|
|
120
|
+
### Supported timestamp formats
|
|
121
|
+
|
|
122
|
+
Timestamps are read from each line to decide which window it belongs to. Two formats are recognised:
|
|
123
|
+
|
|
124
|
+
- **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`
|
|
125
|
+
- **Syslog** — `Sep 17 12:03:10` (syslog has no year, so the current year is assumed)
|
|
126
|
+
|
|
127
|
+
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.
|
|
128
|
+
|
|
129
|
+
### `--explain`
|
|
130
|
+
|
|
131
|
+
Requires an API key for one of three providers, set in your environment:
|
|
132
|
+
|
|
133
|
+
| `--explain-provider` | Key | Default model |
|
|
134
|
+
|---|---|---|
|
|
135
|
+
| `anthropic` (default) | `ANTHROPIC_API_KEY` | `claude-haiku-4-5-20251001` |
|
|
136
|
+
| `openai` | `OPENAI_API_KEY` | `gpt-4o-mini` |
|
|
137
|
+
| `openrouter` | `OPENROUTER_API_KEY` | `anthropic/claude-haiku-4.5` |
|
|
138
|
+
|
|
139
|
+
[OpenRouter](https://openrouter.ai) 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.
|
|
140
|
+
|
|
141
|
+
Only the anomalous window's ERROR and WARN lines are sent (capped at 30 lines). Pick a different model with `--explain-model`:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
export OPENAI_API_KEY=your-key-here
|
|
145
|
+
logometer tail app.log --explain --explain-provider openai --explain-model gpt-4o
|
|
146
|
+
|
|
147
|
+
export OPENROUTER_API_KEY=your-key-here
|
|
148
|
+
logometer tail app.log --explain --explain-provider openrouter --explain-model google/gemini-2.5-flash
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### `--ignore`
|
|
152
|
+
|
|
153
|
+
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:
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
# on a real macOS install log, one benign repeating message accounted for
|
|
157
|
+
# 97% of all "errors" — muting it cut the noise dramatically
|
|
158
|
+
logometer tail /var/log/install.log --replay --window 60 --quiet \
|
|
159
|
+
--ignore 'installation-check' # 27 anomalies -> 5, all genuine
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Each `--ignore` takes a regular expression and can be repeated. An invalid pattern is reported as a normal CLI error rather than a crash.
|
|
163
|
+
|
|
164
|
+
### Live tailing
|
|
165
|
+
|
|
166
|
+
`logometer tail app.log` (without `--replay`) follows the file the way `tail -f` does, with two things a naive tail loop gets wrong:
|
|
167
|
+
|
|
168
|
+
- **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.
|
|
169
|
+
- **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.
|
|
170
|
+
|
|
171
|
+
Output is flushed as each window is reported, so piping or redirecting works in real time:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
logometer tail app.log --quiet --format json >> alerts.jsonl
|
|
175
|
+
tail -f app.log | logometer tail -
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### Pretty output
|
|
179
|
+
|
|
180
|
+
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.
|
|
181
|
+
|
|
182
|
+
## Example output
|
|
183
|
+
|
|
184
|
+
From the included sample log:
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
$ logometer tail examples/sample.log --replay --no-pretty
|
|
188
|
+
|
|
189
|
+
[12:01:20 – 12:01:30] ok errors: 0 baseline: ~0.1
|
|
190
|
+
|
|
191
|
+
[12:01:30 – 12:01:40] ! ANOMALY errors: 10 warns: 1 baseline: ~0.1 score: 9.9x
|
|
192
|
+
New error signature detected: '<x> ERROR ConnectionResetError: [Errno <x>] Connection reset by peer id=<x>'
|
|
193
|
+
|
|
194
|
+
[12:01:40 – 12:01:50] ! ANOMALY errors: 6 baseline: ~0.1 score: 5.9x
|
|
195
|
+
(error rate spike — no brand-new error signature)
|
|
196
|
+
|
|
197
|
+
[12:01:50 – 12:02:00] ok errors: 0 baseline: ~0.1
|
|
198
|
+
...
|
|
199
|
+
-- 18 window(s) processed, 4 anomaly(ies) flagged --
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
`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.
|
|
203
|
+
|
|
204
|
+
With `--explain`, each anomaly gets one more line:
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
[12:01:30 – 12:01:40] ! ANOMALY errors: 10 warns: 1 baseline: ~0.1 score: 9.9x
|
|
208
|
+
New error signature detected: '<x> ERROR ConnectionResetError: [Errno <x>] Connection reset by peer id=<x>'
|
|
209
|
+
Explanation: Database or downstream service became unresponsive, causing clients to forcibly close connections due to timeouts or hangs.
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
With `--format json`, each window is one line with these fields:
|
|
213
|
+
|
|
214
|
+
```json
|
|
215
|
+
{"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}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
The end-of-run summary line is omitted in JSON mode, so every line of output is valid JSON.
|
|
219
|
+
|
|
220
|
+
## How the detection works
|
|
221
|
+
|
|
222
|
+
<!-- Source: docs/flow.mmd. Regenerate after editing it with:
|
|
223
|
+
npx -p @mermaid-js/mermaid-cli mmdc -i docs/flow.mmd -o docs/flow.png -b white -s 2 -c docs/mermaid-config.json -->
|
|
224
|
+

|
|
225
|
+
|
|
226
|
+
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.
|
|
227
|
+
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.
|
|
228
|
+
3. **Window.** Lines are batched into fixed-size windows — by time if timestamps are parseable, otherwise by a fixed line count.
|
|
229
|
+
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.
|
|
230
|
+
5. **Flag.** A window is anomalous if either:
|
|
231
|
+
- 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
|
|
232
|
+
- it contains an error *or warning* shape not seen earlier in this run (checked from the second window on).
|
|
233
|
+
|
|
234
|
+
Two tuning choices keep this honest:
|
|
235
|
+
|
|
236
|
+
- **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.
|
|
237
|
+
- **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.
|
|
238
|
+
|
|
239
|
+
No ML model, no training step — deliberately simple and explainable.
|
|
240
|
+
|
|
241
|
+
## Known limitations
|
|
242
|
+
|
|
243
|
+
Worth knowing before you point this at something you care about. These are real, tested behaviours, not hypotheticals:
|
|
244
|
+
|
|
245
|
+
- **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.
|
|
246
|
+
- **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.
|
|
247
|
+
- **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 -`.
|
|
248
|
+
- **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.
|
|
249
|
+
- **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.
|
|
250
|
+
- **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.
|
|
251
|
+
- **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.)
|
|
252
|
+
- **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.
|
|
253
|
+
- **`--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.
|
|
254
|
+
- **`--explain` sends log lines to a third party.** There's no redaction — don't use it on logs containing secrets or personal data.
|
|
255
|
+
|
|
256
|
+
## Running the tests
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
python3 -m unittest discover -s tests -v
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
No network or API key is needed — the `--explain` tests mock the HTTP call.
|
|
263
|
+
|
|
264
|
+
## Project layout
|
|
265
|
+
|
|
266
|
+
```
|
|
267
|
+
logometer/
|
|
268
|
+
cli.py command-line entry point, file/stdin reading, output formatting
|
|
269
|
+
classifier.py severity tagging and error-shape fingerprinting
|
|
270
|
+
timeparse.py timestamp extraction (ISO 8601, syslog)
|
|
271
|
+
windower.py groups lines into time- or count-based windows
|
|
272
|
+
baseline.py rolling mean / standard deviation of errors per window
|
|
273
|
+
detector.py decides whether a window is anomalous
|
|
274
|
+
explain.py optional LLM explanations (Anthropic / OpenAI / OpenRouter over plain HTTPS)
|
|
275
|
+
pretty.py optional rich-styled output
|
|
276
|
+
tests/ unit and end-to-end tests
|
|
277
|
+
examples/ sample log with an injected error burst
|
|
278
|
+
docs/ flow diagram (Mermaid source + rendered PNG)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Roadmap
|
|
282
|
+
|
|
283
|
+
- Config file for custom log-format parsing
|
|
284
|
+
- Multiple file / glob support
|
|
285
|
+
- Slack/Discord webhook alerts
|
|
286
|
+
- Persistent anomaly history (SQLite)
|
|
287
|
+
|
|
288
|
+
## Contributing
|
|
289
|
+
|
|
290
|
+
Issues and PRs welcome — especially around log-format parsing (every stack logs differently) and reducing false positives.
|
|
291
|
+
|
|
292
|
+
## License
|
|
293
|
+
|
|
294
|
+
MIT — see [LICENSE](https://github.com/AmanSg098/logometer/blob/main/LICENSE).
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# logometer
|
|
2
|
+
|
|
3
|
+
**Tail your logs. Catch anomalies. Skip the 2am grep.**
|
|
4
|
+
|
|
5
|
+
`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.
|
|
6
|
+
|
|
7
|
+
Zero dependencies for the core tool — just Python 3.10+.
|
|
8
|
+
|
|
9
|
+
<!-- TODO: record a short asciinema/GIF demo and drop it here before publishing.
|
|
10
|
+
Suggested recording: `logometer tail examples/sample.log --replay` -->
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
As a standalone command, without cloning:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pipx install git+https://github.com/AmanSg098/logometer.git
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Or from a clone, for development:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
git clone https://github.com/AmanSg098/logometer.git
|
|
24
|
+
cd logometer
|
|
25
|
+
pip install -e . # core tool, no dependencies
|
|
26
|
+
pip install -e ".[pretty]" # optional: styled output via rich
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
Try it on the included sample log (has a normal-traffic baseline plus an injected error burst):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python3 -m logometer.cli tail examples/sample.log --replay
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
or, once installed:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
logometer tail examples/sample.log --replay
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
# Tail a live log file (like tail -f, but with anomaly detection)
|
|
47
|
+
logometer tail /var/log/app.log
|
|
48
|
+
|
|
49
|
+
# Pipe from stdin
|
|
50
|
+
tail -f app.log | logometer tail -
|
|
51
|
+
|
|
52
|
+
# Replay a static file from start to finish, then exit
|
|
53
|
+
logometer tail app.log --replay
|
|
54
|
+
|
|
55
|
+
# Only show anomalous windows, hide the "ok" noise
|
|
56
|
+
logometer tail app.log --replay --quiet
|
|
57
|
+
|
|
58
|
+
# Adjust window size (seconds) and how aggressively to flag deviations
|
|
59
|
+
logometer tail app.log --window 30 --sensitivity high
|
|
60
|
+
|
|
61
|
+
# Mute known-noisy lines so they never reach the baseline (repeatable regex)
|
|
62
|
+
logometer tail app.log --ignore 'healthcheck' --ignore 'DeprecationWarning'
|
|
63
|
+
|
|
64
|
+
# Ask an LLM for a one-sentence explanation of each anomaly
|
|
65
|
+
export ANTHROPIC_API_KEY=your-key-here
|
|
66
|
+
logometer tail app.log --explain
|
|
67
|
+
|
|
68
|
+
# JSON output, one object per line — good for piping into other tools
|
|
69
|
+
logometer tail app.log --format json
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Stop a live tail with Ctrl-C (or `SIGTERM` from a process manager); the window in progress is reported before exiting.
|
|
73
|
+
|
|
74
|
+
### All options
|
|
75
|
+
|
|
76
|
+
| Option | Default | What it does |
|
|
77
|
+
|---|---|---|
|
|
78
|
+
| `file` | — | Log file to read, or `-` for stdin |
|
|
79
|
+
| `--window SECONDS` | `10` | Window size, when timestamps are parseable |
|
|
80
|
+
| `--sensitivity` | `medium` | `low`, `medium` or `high` — how far above the baseline a window must be to count as a spike |
|
|
81
|
+
| `--format` | `plain` | `plain` or `json` |
|
|
82
|
+
| `--replay` | off | Read the file start to end and exit, instead of following it live |
|
|
83
|
+
| `--quiet` | off | Only print anomalous windows |
|
|
84
|
+
| `--ignore REGEX` | — | Drop matching lines before analysis; repeatable |
|
|
85
|
+
| `--explain` | off | Ask an LLM for a one-sentence cause of each anomaly |
|
|
86
|
+
| `--explain-provider` | `anthropic` | `anthropic`, `openai` or `openrouter` |
|
|
87
|
+
| `--explain-model MODEL` | see below | Override the model used by `--explain` |
|
|
88
|
+
| `--no-pretty` | off | Use plain output even if `rich` is installed |
|
|
89
|
+
| `--version` | — | Print the version and exit |
|
|
90
|
+
|
|
91
|
+
### Supported timestamp formats
|
|
92
|
+
|
|
93
|
+
Timestamps are read from each line to decide which window it belongs to. Two formats are recognised:
|
|
94
|
+
|
|
95
|
+
- **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`
|
|
96
|
+
- **Syslog** — `Sep 17 12:03:10` (syslog has no year, so the current year is assumed)
|
|
97
|
+
|
|
98
|
+
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.
|
|
99
|
+
|
|
100
|
+
### `--explain`
|
|
101
|
+
|
|
102
|
+
Requires an API key for one of three providers, set in your environment:
|
|
103
|
+
|
|
104
|
+
| `--explain-provider` | Key | Default model |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| `anthropic` (default) | `ANTHROPIC_API_KEY` | `claude-haiku-4-5-20251001` |
|
|
107
|
+
| `openai` | `OPENAI_API_KEY` | `gpt-4o-mini` |
|
|
108
|
+
| `openrouter` | `OPENROUTER_API_KEY` | `anthropic/claude-haiku-4.5` |
|
|
109
|
+
|
|
110
|
+
[OpenRouter](https://openrouter.ai) 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.
|
|
111
|
+
|
|
112
|
+
Only the anomalous window's ERROR and WARN lines are sent (capped at 30 lines). Pick a different model with `--explain-model`:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
export OPENAI_API_KEY=your-key-here
|
|
116
|
+
logometer tail app.log --explain --explain-provider openai --explain-model gpt-4o
|
|
117
|
+
|
|
118
|
+
export OPENROUTER_API_KEY=your-key-here
|
|
119
|
+
logometer tail app.log --explain --explain-provider openrouter --explain-model google/gemini-2.5-flash
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### `--ignore`
|
|
123
|
+
|
|
124
|
+
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:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
# on a real macOS install log, one benign repeating message accounted for
|
|
128
|
+
# 97% of all "errors" — muting it cut the noise dramatically
|
|
129
|
+
logometer tail /var/log/install.log --replay --window 60 --quiet \
|
|
130
|
+
--ignore 'installation-check' # 27 anomalies -> 5, all genuine
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Each `--ignore` takes a regular expression and can be repeated. An invalid pattern is reported as a normal CLI error rather than a crash.
|
|
134
|
+
|
|
135
|
+
### Live tailing
|
|
136
|
+
|
|
137
|
+
`logometer tail app.log` (without `--replay`) follows the file the way `tail -f` does, with two things a naive tail loop gets wrong:
|
|
138
|
+
|
|
139
|
+
- **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.
|
|
140
|
+
- **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.
|
|
141
|
+
|
|
142
|
+
Output is flushed as each window is reported, so piping or redirecting works in real time:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
logometer tail app.log --quiet --format json >> alerts.jsonl
|
|
146
|
+
tail -f app.log | logometer tail -
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Pretty output
|
|
150
|
+
|
|
151
|
+
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.
|
|
152
|
+
|
|
153
|
+
## Example output
|
|
154
|
+
|
|
155
|
+
From the included sample log:
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
$ logometer tail examples/sample.log --replay --no-pretty
|
|
159
|
+
|
|
160
|
+
[12:01:20 – 12:01:30] ok errors: 0 baseline: ~0.1
|
|
161
|
+
|
|
162
|
+
[12:01:30 – 12:01:40] ! ANOMALY errors: 10 warns: 1 baseline: ~0.1 score: 9.9x
|
|
163
|
+
New error signature detected: '<x> ERROR ConnectionResetError: [Errno <x>] Connection reset by peer id=<x>'
|
|
164
|
+
|
|
165
|
+
[12:01:40 – 12:01:50] ! ANOMALY errors: 6 baseline: ~0.1 score: 5.9x
|
|
166
|
+
(error rate spike — no brand-new error signature)
|
|
167
|
+
|
|
168
|
+
[12:01:50 – 12:02:00] ok errors: 0 baseline: ~0.1
|
|
169
|
+
...
|
|
170
|
+
-- 18 window(s) processed, 4 anomaly(ies) flagged --
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`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.
|
|
174
|
+
|
|
175
|
+
With `--explain`, each anomaly gets one more line:
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
[12:01:30 – 12:01:40] ! ANOMALY errors: 10 warns: 1 baseline: ~0.1 score: 9.9x
|
|
179
|
+
New error signature detected: '<x> ERROR ConnectionResetError: [Errno <x>] Connection reset by peer id=<x>'
|
|
180
|
+
Explanation: Database or downstream service became unresponsive, causing clients to forcibly close connections due to timeouts or hangs.
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
With `--format json`, each window is one line with these fields:
|
|
184
|
+
|
|
185
|
+
```json
|
|
186
|
+
{"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}
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The end-of-run summary line is omitted in JSON mode, so every line of output is valid JSON.
|
|
190
|
+
|
|
191
|
+
## How the detection works
|
|
192
|
+
|
|
193
|
+
<!-- Source: docs/flow.mmd. Regenerate after editing it with:
|
|
194
|
+
npx -p @mermaid-js/mermaid-cli mmdc -i docs/flow.mmd -o docs/flow.png -b white -s 2 -c docs/mermaid-config.json -->
|
|
195
|
+

|
|
196
|
+
|
|
197
|
+
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.
|
|
198
|
+
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.
|
|
199
|
+
3. **Window.** Lines are batched into fixed-size windows — by time if timestamps are parseable, otherwise by a fixed line count.
|
|
200
|
+
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.
|
|
201
|
+
5. **Flag.** A window is anomalous if either:
|
|
202
|
+
- 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
|
|
203
|
+
- it contains an error *or warning* shape not seen earlier in this run (checked from the second window on).
|
|
204
|
+
|
|
205
|
+
Two tuning choices keep this honest:
|
|
206
|
+
|
|
207
|
+
- **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.
|
|
208
|
+
- **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.
|
|
209
|
+
|
|
210
|
+
No ML model, no training step — deliberately simple and explainable.
|
|
211
|
+
|
|
212
|
+
## Known limitations
|
|
213
|
+
|
|
214
|
+
Worth knowing before you point this at something you care about. These are real, tested behaviours, not hypotheticals:
|
|
215
|
+
|
|
216
|
+
- **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.
|
|
217
|
+
- **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.
|
|
218
|
+
- **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 -`.
|
|
219
|
+
- **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.
|
|
220
|
+
- **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.
|
|
221
|
+
- **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.
|
|
222
|
+
- **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.)
|
|
223
|
+
- **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.
|
|
224
|
+
- **`--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.
|
|
225
|
+
- **`--explain` sends log lines to a third party.** There's no redaction — don't use it on logs containing secrets or personal data.
|
|
226
|
+
|
|
227
|
+
## Running the tests
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
python3 -m unittest discover -s tests -v
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
No network or API key is needed — the `--explain` tests mock the HTTP call.
|
|
234
|
+
|
|
235
|
+
## Project layout
|
|
236
|
+
|
|
237
|
+
```
|
|
238
|
+
logometer/
|
|
239
|
+
cli.py command-line entry point, file/stdin reading, output formatting
|
|
240
|
+
classifier.py severity tagging and error-shape fingerprinting
|
|
241
|
+
timeparse.py timestamp extraction (ISO 8601, syslog)
|
|
242
|
+
windower.py groups lines into time- or count-based windows
|
|
243
|
+
baseline.py rolling mean / standard deviation of errors per window
|
|
244
|
+
detector.py decides whether a window is anomalous
|
|
245
|
+
explain.py optional LLM explanations (Anthropic / OpenAI / OpenRouter over plain HTTPS)
|
|
246
|
+
pretty.py optional rich-styled output
|
|
247
|
+
tests/ unit and end-to-end tests
|
|
248
|
+
examples/ sample log with an injected error burst
|
|
249
|
+
docs/ flow diagram (Mermaid source + rendered PNG)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Roadmap
|
|
253
|
+
|
|
254
|
+
- Config file for custom log-format parsing
|
|
255
|
+
- Multiple file / glob support
|
|
256
|
+
- Slack/Discord webhook alerts
|
|
257
|
+
- Persistent anomaly history (SQLite)
|
|
258
|
+
|
|
259
|
+
## Contributing
|
|
260
|
+
|
|
261
|
+
Issues and PRs welcome — especially around log-format parsing (every stack logs differently) and reducing false positives.
|
|
262
|
+
|
|
263
|
+
## License
|
|
264
|
+
|
|
265
|
+
MIT — see [LICENSE](https://github.com/AmanSg098/logometer/blob/main/LICENSE).
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""
|
|
2
|
+
A small rolling baseline: keeps the last N observed values for a metric
|
|
3
|
+
(e.g. error count per window) and reports the mean/std of that history.
|
|
4
|
+
|
|
5
|
+
Deliberately simple — a moving window of raw values, not an
|
|
6
|
+
exponentially-weighted or Bayesian estimator. Easy to explain, easy to
|
|
7
|
+
reason about when it gets something wrong.
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import statistics
|
|
12
|
+
from collections import deque
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RollingBaseline:
|
|
16
|
+
def __init__(self, history_size: int = 20):
|
|
17
|
+
"""Keep a fixed-size deque of recent metric samples (e.g. errors per window).
|
|
18
|
+
Older values drop off automatically when history_size is exceeded."""
|
|
19
|
+
self._values: deque[float] = deque(maxlen=history_size)
|
|
20
|
+
|
|
21
|
+
def update(self, value: float) -> None:
|
|
22
|
+
"""Append one observation; drop the oldest when history exceeds history_size.
|
|
23
|
+
Called after each non-spike window in AnomalyDetector.evaluate."""
|
|
24
|
+
self._values.append(value)
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def ready(self) -> bool:
|
|
28
|
+
"""True once there are enough samples to compare against (>= 3).
|
|
29
|
+
Until ready, deviation_score returns 0 and spikes are not scored."""
|
|
30
|
+
return len(self._values) >= 3
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def mean(self) -> float:
|
|
34
|
+
"""Arithmetic mean of stored samples, or 0.0 if history is empty.
|
|
35
|
+
Shown in CLI output as the approximate errors-per-window baseline."""
|
|
36
|
+
if not self._values:
|
|
37
|
+
return 0.0
|
|
38
|
+
return statistics.fmean(self._values)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def stdev(self) -> float:
|
|
42
|
+
"""Population standard deviation of stored samples, or 0.0 if fewer than two.
|
|
43
|
+
Used with mean inside deviation_score for spike detection."""
|
|
44
|
+
if len(self._values) < 2:
|
|
45
|
+
return 0.0
|
|
46
|
+
return statistics.pstdev(self._values)
|
|
47
|
+
|
|
48
|
+
def deviation_score(self, value: float, min_stdev: float = 1.0) -> float:
|
|
49
|
+
"""Return how many std devs above the mean `value` is (0 if not ready).
|
|
50
|
+
Uses max(stdev, min_stdev) so flat histories do not over-flag single events."""
|
|
51
|
+
if not self.ready:
|
|
52
|
+
return 0.0
|
|
53
|
+
std = max(self.stdev, min_stdev)
|
|
54
|
+
return (value - self.mean) / std
|