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.
@@ -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.
@@ -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
+ ![How logometer processes a log: prepare each line, judge each window, report](https://raw.githubusercontent.com/AmanSg098/logometer/main/docs/flow.png)
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
+ ![How logometer processes a log: prepare each line, judge each window, report](https://raw.githubusercontent.com/AmanSg098/logometer/main/docs/flow.png)
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,3 @@
1
+ """logometer — tail your logs, catch anomalies, skip the 2am grep."""
2
+
3
+ __version__ = "0.1.0"
@@ -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