firewatcher 1.60__tar.gz → 1.63__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.
- firewatcher-1.63/PKG-INFO +257 -0
- firewatcher-1.63/README.md +230 -0
- {firewatcher-1.60 → firewatcher-1.63}/examples/firewatcher.conf +22 -1
- {firewatcher-1.60 → firewatcher-1.63}/examples/sys_msg.txt +3 -1
- firewatcher-1.63/firewatcher.egg-info/PKG-INFO +257 -0
- {firewatcher-1.60 → firewatcher-1.63}/firewatcher.egg-info/SOURCES.txt +1 -0
- firewatcher-1.63/firewatcher.py +2922 -0
- {firewatcher-1.60 → firewatcher-1.63}/tests/conftest.py +25 -3
- firewatcher-1.63/tests/test_capture.py +1463 -0
- firewatcher-1.63/tests/test_cli.py +371 -0
- firewatcher-1.63/tests/test_compressor.py +231 -0
- firewatcher-1.63/tests/test_config.py +572 -0
- {firewatcher-1.60 → firewatcher-1.63}/tests/test_log_source.py +29 -4
- {firewatcher-1.60 → firewatcher-1.63}/tests/test_matching.py +28 -1
- {firewatcher-1.60 → firewatcher-1.63}/tests/test_notify.py +307 -0
- firewatcher-1.63/tests/test_position.py +631 -0
- {firewatcher-1.60 → firewatcher-1.63}/tests/test_systemd.py +105 -0
- firewatcher-1.60/PKG-INFO +0 -181
- firewatcher-1.60/README.md +0 -154
- firewatcher-1.60/firewatcher.egg-info/PKG-INFO +0 -181
- firewatcher-1.60/firewatcher.py +0 -1470
- firewatcher-1.60/tests/test_capture.py +0 -347
- firewatcher-1.60/tests/test_cli.py +0 -154
- firewatcher-1.60/tests/test_compressor.py +0 -102
- firewatcher-1.60/tests/test_config.py +0 -320
- {firewatcher-1.60 → firewatcher-1.63}/LICENSE +0 -0
- {firewatcher-1.60 → firewatcher-1.63}/MANIFEST.in +0 -0
- {firewatcher-1.60 → firewatcher-1.63}/examples/nvme_failure.regex +0 -0
- {firewatcher-1.60 → firewatcher-1.63}/firewatcher.egg-info/dependency_links.txt +0 -0
- {firewatcher-1.60 → firewatcher-1.63}/firewatcher.egg-info/entry_points.txt +0 -0
- {firewatcher-1.60 → firewatcher-1.63}/firewatcher.egg-info/top_level.txt +0 -0
- {firewatcher-1.60 → firewatcher-1.63}/setup.cfg +0 -0
- {firewatcher-1.60 → firewatcher-1.63}/setup.py +0 -0
- {firewatcher-1.60 → firewatcher-1.63}/tests/test_patterns.py +0 -0
- {firewatcher-1.60 → firewatcher-1.63}/tests/test_slugify.py +0 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: firewatcher
|
|
3
|
+
Version: 1.63
|
|
4
|
+
Summary: Watch logs for incident patterns and persist surrounding context off-box
|
|
5
|
+
Home-page: https://github.com/yufei-pan/firewatcher
|
|
6
|
+
Author: Yufei Pan
|
|
7
|
+
Author-email: pan@zopyr.us
|
|
8
|
+
License: GPLv3+
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Topic :: System :: Logging
|
|
13
|
+
Classifier: Topic :: System :: Monitoring
|
|
14
|
+
Requires-Python: >=3.6
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: author
|
|
18
|
+
Dynamic: author-email
|
|
19
|
+
Dynamic: classifier
|
|
20
|
+
Dynamic: description
|
|
21
|
+
Dynamic: description-content-type
|
|
22
|
+
Dynamic: home-page
|
|
23
|
+
Dynamic: license
|
|
24
|
+
Dynamic: license-file
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
# firewatcher
|
|
29
|
+
|
|
30
|
+
Watch a log stream (journald, syslog, or any file) for incident patterns, then persist a window of surrounding messages to a durable location — including a network filesystem — so the record survives even if the machine later dies.
|
|
31
|
+
|
|
32
|
+
The watcher reads the journal with `journalctl --follow` when journald runs on the host (or its journal files are there, such as a host journal mounted into a container). Otherwise it follows `/var/log/syslog`, then `/var/log/messages`, even when `journalctl` is installed; `--log-file` picks a file. With no syslog file either, it falls back to `journalctl`. Running the daemon does not require systemd; only `--install-service` / `--uninstall-service` do.
|
|
33
|
+
|
|
34
|
+
Everything but [resuming after a restart](#resuming-after-a-restart) works with every source: the compressed context before a match, leaving out context already captured, the free-space floor, SIGHUP reload, and LLM analysis and pushes. Resuming needs the journal; with a log file, firewatcher reads the last `--tail-lines` lines at every start, as before.
|
|
35
|
+
|
|
36
|
+
## Install
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install firewatcher
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
From a clone of this repository:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install .
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Requires **Python 3.6+**. No third-party runtime dependencies.
|
|
49
|
+
|
|
50
|
+
## Quick start
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
firewatcher /etc/firewatcher/patterns.d
|
|
54
|
+
firewatcher -t 300 -o /var/log/captured_messages/ /etc/firewatcher/patterns.d
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
A directory argument loads every non-hidden pattern file inside it. Files ending in `.regex` are compiled as regular expressions. Any other pattern file is treated as fnmatch (bash-like) substrings, with `*` added on both ends. Lines starting with `#` are comments; to match text that starts with `#`, such as the kernel's `#PF:`, write `[#]PF:` in a plain file or `\#PF:` in a `.regex` file. Matching is case-sensitive; for a case-insensitive pattern, put it in a `.regex` file with `(?i)` in front.
|
|
58
|
+
|
|
59
|
+
`firewatcher` exits with an error when no pattern loads, for example when a pattern path is mistyped or every regex is invalid, instead of running while matching nothing.
|
|
60
|
+
|
|
61
|
+
`--install-service` seeds `/etc/firewatcher/patterns.d` with example patterns when that directory is empty. The `examples/` directory in the source tree is not installed onto `PATH` by `pip install`; copy the pattern files from a clone, or let `--install-service` write them.
|
|
62
|
+
|
|
63
|
+
On a match, `firewatcher` writes:
|
|
64
|
+
|
|
65
|
+
- a per-incident capture under `{output-folder}/{YYYY-MM}/…`
|
|
66
|
+
- a line in `{output-folder}/journal.log`
|
|
67
|
+
|
|
68
|
+
Live capture is written as lines arrive, so a copy can already be on another filesystem if the host then disappears. Each captured line is flushed as it is written, and the capture file is fsynced at most once a second and when the capture closes, so a host crash loses at most about the last second of it.
|
|
69
|
+
|
|
70
|
+
A capture holds the whole `--capture-time` window before its match, so on a busy host one capture can be tens of megabytes; a lower `--capture-time` or `--buffer-max-mb` keeps captures smaller.
|
|
71
|
+
|
|
72
|
+
When a capture opens, lines of its window that an earlier capture of the same run already holds are left out, with a note naming that file and how many lines it holds. In an error storm, where each capture opens right after the last one closes, that saves writing the same context again and again; lines that no capture holds are always written. Such a capture is not complete on its own: its LLM excerpt and push see the note, not the lines in the earlier file. A capture that moved to the fallback folder, or lost lines on the way, is never pointed to: later captures write its lines again.
|
|
73
|
+
|
|
74
|
+
A capture ends `--capture-time` seconds after its last matching line, or `--capture-time-max` seconds (default 3600) after it opened, whichever comes first, so a pattern that keeps repeating still produces a finished capture, and its analysis and push, at least once an hour. A later match starts a new capture.
|
|
75
|
+
|
|
76
|
+
If the output folder cannot be written (a full disk, a read-only remount, an NFS server that returns errors), the capture continues under `--fallback-folder` (default `/var/lib/firewatcher/captured_messages/`), starting with every line written since the last successful fsync and a note naming the file it moved from. That capture is listed in the fallback folder's own `journal.log`, and the fallback folder is compressed and pruned like the output folder. If no folder can be written, `firewatcher` keeps running, logs the error, and still hands the incident to the notifier.
|
|
77
|
+
|
|
78
|
+
Put the fallback folder on a different filesystem from the output folder. The default is under `/var/lib`, which shares a disk with `/var/log` on most systems, so it covers a failed NFS output folder but not a full local disk.
|
|
79
|
+
|
|
80
|
+
The fallback needs the failure to come back as an error. On an NFS mount with the default `hard` option, writes to an unreachable server wait instead of failing, and `firewatcher` waits with them. To fall back instead, mount the output folder with `soft` (or `softerr`) and a short `timeo`.
|
|
81
|
+
|
|
82
|
+
`firewatcher` finishes the open capture on SIGINT and on SIGTERM. A second SIGTERM ends it at once.
|
|
83
|
+
|
|
84
|
+
SIGHUP reloads the pattern files and the `--always-notify` files from the paths given at startup, so after editing `/etc/firewatcher/patterns.d/`, run `sudo systemctl reload firewatcher`. The new patterns apply from the next log line, and the journal says how many loaded. If no watch pattern loads, the old ones stay and the error is logged. The config file and other settings are not re-read; restart for those. In a terminal, SIGHUP keeps its usual meaning (the terminal went away) and ends `firewatcher`. Two SIGHUPs close together reload once.
|
|
85
|
+
|
|
86
|
+
### Resuming after a restart
|
|
87
|
+
|
|
88
|
+
When firewatcher follows the journal (not a log file), it saves how far it has read in `--position-file` (default `/var/lib/firewatcher/journal.position`): about once a second while lines arrive or a capture is open, as soon as a match opens a capture, and when it stops. After a restart it reads on from there instead of the last `--tail-lines` lines; after a crash or `kill -9`, that can be up to about a second before the last line it read. Lines logged while it was stopped, such as during a reboot, are still matched, and an incident it already captured is not captured again. The `--capture-time` seconds before the saved line are read again only to refill the context kept before a match. At most `--resume-max` seconds (default 3600) are read back, so after a longer stop only the last hour is matched.
|
|
89
|
+
|
|
90
|
+
To tell where it stopped, firewatcher reads the journal with `journalctl -o short-iso`, so journal lines in captures carry the year and time zone: `2026-10-23T17:52:41+00:00 host sshd[812]: …` instead of `Oct 23 17:52:41 host sshd[812]: …`. Lines logged while firewatcher was stopped arrive all at once. A capture opened by one of them takes its context from the `--capture-time` before that line by the time each line was logged, and holds the lines after it as they are read, up to `--capture-line-count-max`.
|
|
91
|
+
|
|
92
|
+
The position is the second of the last line read, which line of that second it was, and a digest of that line. When unsure, firewatcher reads a line again rather than skip it. A run that started from `--tail-lines` in the middle of a second numbers that second's lines from where it started, so identical lines logged in that second can be captured again after a restart. A save that fails is logged once, and a restart then reads again from the last position saved. If the clock is set back after a position is saved, for example at a boot with a wrong hardware clock, new lines stamped before that second count as already read. A position more than a minute in the future is ignored.
|
|
93
|
+
|
|
94
|
+
Only one firewatcher uses a position file at a time: another one started with the same file, such as a test run in a terminal with the default settings, reads the last `--tail-lines` lines and saves nothing. An empty `--position-file` turns resuming off, and so does a journalctl that does not accept `-o short-iso` and `--since=@`, which firewatcher checks at start.
|
|
95
|
+
|
|
96
|
+
### Disk and memory limits
|
|
97
|
+
|
|
98
|
+
firewatcher keeps the filesystems it writes captures to above `--min-free-percent` free (default 1%, counted as `df` does, so the root-reserved blocks are left alone). Only what the filesystem reports authorizes a write. firewatcher checks when a capture opens, about once a second, and again before what it has written since the last check could bring it within 64 KiB of the floor (and at least every 8 MiB), fsyncing first so the filesystem, an NFS server included, counts every byte. A line that would cross that margin is not written there. Another program filling the same disk between two checks can still push it past the floor; firewatcher's own writes do not.
|
|
99
|
+
|
|
100
|
+
Near the floor, firewatcher may delete its own oldest months in that folder, so a new incident can be recorded, under these rules:
|
|
101
|
+
|
|
102
|
+
- **What:** only what firewatcher itself creates: `YYYY-MM` folders, `YYYY-MM[.N].tar.xz` archives, and their `.partial` files, each of the kind it should be (a folder, or a regular file). A month counts as a whole.
|
|
103
|
+
- **Never:** the current month, the month being written, a month with anything in it changed in the last day (another firewatcher may be writing there), a month being compressed, or a month with another filesystem mounted inside it.
|
|
104
|
+
- **How much:** only as many months as it takes, counted by their size on disk, and nothing at all if deleting every one of them still could not make room. That happens when something else filled the disk, and then the history is kept.
|
|
105
|
+
- **How often:** at most once per `--purge-interval` per folder (default an hour), so a disk that something else keeps filling drains firewatcher's history slowly. A purge that could not help is not tried again for five minutes (or the interval, if shorter). Each deletion is logged.
|
|
106
|
+
|
|
107
|
+
After deleting, firewatcher syncs and reads the free space again. A filesystem that shows freed space late costs a move to the fallback folder, never the floor. If there is still no room, the capture moves to the fallback folder with a note, or, with no folder left, stops being written, which is logged; the incident is still pushed. Compression keeps to the floor too: a month whose archive could cross it is left uncompressed for now. Purging and compression never wait for each other, so log reading never waits for `tar`. `--delete-after-months 0` means nothing is deleted by age; the floor can still delete the oldest months. Set `--min-free-percent 0` to turn all of this off.
|
|
108
|
+
|
|
109
|
+
Memory is bounded too. The `--capture-time` seconds before a match are kept in memory compressed (zlib, in blocks of about 64 KiB of text; with its bookkeeping, about 3 times smaller than the text for a busy host's varied lines, more for repetitive ones), within `--buffer-max-mb` (default 16 MiB). Past the limit the oldest block is dropped and the capture says so. The newest lines, up to 256 KiB of them before they are compressed, are always kept, so the buffer can go over the limit by about that much, or by one very long line. Compression costs well under a microsecond per line. A line longer than 1 MiB is read in pieces, and each piece is matched on its own, so a pattern split across the 1 MiB boundary is missed. The LLM excerpt reads at most `max_bytes` of a capture.
|
|
110
|
+
|
|
111
|
+
Monthly directories are compressed on a background thread, so log reading never waits for `tar`. Each month becomes `YYYY-MM.tar.xz` holding `YYYY-MM/…`. The directory is removed only after `tar` succeeds and the archive is synced to disk. An existing archive is never overwritten: the new one becomes `YYYY-MM.1.tar.xz`, and so on.
|
|
112
|
+
|
|
113
|
+
## systemd service
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
sudo firewatcher --install-service
|
|
117
|
+
sudo firewatcher --print-unit
|
|
118
|
+
sudo firewatcher --uninstall-service
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`--install-service` writes `/etc/systemd/system/firewatcher.service`, creates the output directory, seeds `/etc/firewatcher/patterns.d/` with example patterns **only if that directory is empty**, writes `/etc/firewatcher/firewatcher.conf` **only if that file is missing**, then `systemctl daemon-reload && systemctl enable --now firewatcher`.
|
|
122
|
+
|
|
123
|
+
`systemctl reload firewatcher` reloads the pattern files without a restart; see SIGHUP above.
|
|
124
|
+
|
|
125
|
+
Capture files follow the process umask. A systemd service defaults to `UMask=0022`, so captures are readable by every local user, while the journal they copy from is normally readable only by root and the `adm`/`systemd-journal` groups. On a host with untrusted local users, add `UMask=0027` to the unit, for example with `systemctl edit firewatcher`.
|
|
126
|
+
|
|
127
|
+
If systemd is not available, `--install-service` and `--uninstall-service` print a warning and exit. `--print-unit` still works so you can copy the unit elsewhere.
|
|
128
|
+
|
|
129
|
+
Useful flags:
|
|
130
|
+
|
|
131
|
+
| Flag | Description |
|
|
132
|
+
|------|-------------|
|
|
133
|
+
| `--unit-name NAME` | Unit name (default: `firewatcher`) |
|
|
134
|
+
| `--requires-mounts-for PATH` | Add `RequiresMountsFor=` (repeat for NFS/remote output) |
|
|
135
|
+
| `--no-enable` | Write the unit and reload, but do not enable or start it |
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
sudo firewatcher --install-service -o /mnt/logs/captured_messages \
|
|
139
|
+
--requires-mounts-for /mnt/logs/captured_messages \
|
|
140
|
+
/etc/firewatcher/patterns.d
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
## Options
|
|
144
|
+
|
|
145
|
+
| Flag | Description |
|
|
146
|
+
|------|-------------|
|
|
147
|
+
| `pattern_file …` | Pattern files or directories |
|
|
148
|
+
| `--log-file` | Log file to follow (default: the journal when journald runs, else `/var/log/syslog`, else `/var/log/messages`) |
|
|
149
|
+
| `-t`, `--capture-time` | Seconds of context after a match (default: 300) |
|
|
150
|
+
| `--capture-time-max` | Longest one capture may run, in seconds (default: 3600; 0 = no limit) |
|
|
151
|
+
| `-o`, `--output-folder` | Where to write captures (default: `/var/log/captured_messages/`) |
|
|
152
|
+
| `--fallback-folder` | Where a capture continues when the output folder cannot be written (default: `/var/lib/firewatcher/captured_messages/`; `''` disables) |
|
|
153
|
+
| `--tail-lines` | Lines to start with (default: 20; `+N` from line N, not with journalctl). Not used when the journal resumes from a saved position |
|
|
154
|
+
| `--filter-only` | Filter existing logs only (`cat` instead of `tail -F`, or `journalctl` without `--follow`). Windows are measured in read time, not log time, and existing logs are read far faster than they were written, so `-t` and `--capture-time-max` barely limit a capture: it usually runs to `--capture-line-count-max` lines or the end of the input |
|
|
155
|
+
| `--compress-after-months` | Tar.xz monthly dirs after N months (default: 3) |
|
|
156
|
+
| `--delete-after-months` | Delete `YYYY-MM` dirs after N months (default: 0 = never by age; the free-space floor can still delete the oldest months) |
|
|
157
|
+
| `--capture-line-count-max` | Max lines written after a match (default: 10000). The lines before it are bounded by `-t` and `--buffer-max-mb` |
|
|
158
|
+
| `--buffer-max-mb` | Memory, in MiB, for the lines kept before a match, compressed (default: 16). The oldest are dropped past it |
|
|
159
|
+
| `--min-free-percent` | Free-space floor, 0–50%, for the filesystems captures go to (default: 1; 0 disables). See [Disk and memory limits](#disk-and-memory-limits) |
|
|
160
|
+
| `--purge-interval SEC` | Least time between two deletions of old months in one folder for free space (default: 3600; 0: no wait) |
|
|
161
|
+
| `--position-file PATH` | Where firewatcher saves how far it read the journal, so a restart resumes there (default: `/var/lib/firewatcher/journal.position`; `''` disables). See [Resuming after a restart](#resuming-after-a-restart) |
|
|
162
|
+
| `--resume-max SEC` | Most seconds of journal read back at start: lines logged while stopped, and context before them (default: 3600; 0: none) |
|
|
163
|
+
| `--config PATH` | INI config file. Default: `/etc/firewatcher/firewatcher.conf` when that file exists. An explicit path must exist |
|
|
164
|
+
| `--llm` / `--no-llm` | Enable or disable LLM analysis of each finished capture |
|
|
165
|
+
| `--llm-base-url URL` | OpenAI-compatible API root, such as `http://127.0.0.1:11434/v1` |
|
|
166
|
+
| `--llm-model NAME` | Model name |
|
|
167
|
+
| `--llm-timeout SEC` | LLM HTTP timeout (default: 60) |
|
|
168
|
+
| `--llm-max-bytes N` | Capture bytes sent to the model (default: 48000) |
|
|
169
|
+
| `--llm-notes TEXT` | A short note about this host, sent to the model with each capture (default: none; `''` clears a note set in the config). See [LLM analysis and notifications](#llm-analysis-and-notifications) |
|
|
170
|
+
| `--notify` / `--no-notify` | Enable or disable ntfy pushes |
|
|
171
|
+
| `--ntfy-url URL` | ntfy topic URL, such as `https://ntfy.sh/mytopic` |
|
|
172
|
+
| `--ntfy-priority N` | ntfy priority 1–5 (default: 4) |
|
|
173
|
+
| `--ntfy-timeout SEC` | Worker ntfy timeout (default: 10). A full analysis queue uses 5 seconds |
|
|
174
|
+
| `--always-notify PATH` | Pattern file or directory (repeatable). A capture with a matched line that also matches one of these is pushed even when the LLM says not to notify |
|
|
175
|
+
| `--install-service` | Install and enable a systemd unit |
|
|
176
|
+
| `--print-unit` | Print that unit and exit |
|
|
177
|
+
| `--uninstall-service` | Disable and remove the unit |
|
|
178
|
+
| `-V`, `--version` | Show version and exit |
|
|
179
|
+
|
|
180
|
+
`--tail_lines`, `--filter_only`, `--no-filter_only` and `--capture_line_count_max` are still accepted with underscores.
|
|
181
|
+
|
|
182
|
+
## Config file
|
|
183
|
+
|
|
184
|
+
Runtime settings live in an INI file. Explicit command-line flags override the file. Built-in defaults apply when neither sets a value. A missing default file is ignored. `--uninstall-service` does not read the file.
|
|
185
|
+
|
|
186
|
+
A commented example with all supported settings is in [`examples/firewatcher.conf`](examples/firewatcher.conf). Copy it to `/etc/firewatcher/firewatcher.conf`, or use `firewatcher --config /path/to/firewatcher.conf`.
|
|
187
|
+
|
|
188
|
+
```ini
|
|
189
|
+
[watch]
|
|
190
|
+
patterns = /etc/firewatcher/patterns.d
|
|
191
|
+
capture_time = 300
|
|
192
|
+
capture_time_max = 3600
|
|
193
|
+
output_folder = /var/log/captured_messages/
|
|
194
|
+
fallback_folder = /var/lib/firewatcher/captured_messages/
|
|
195
|
+
filter_only = false
|
|
196
|
+
|
|
197
|
+
[llm]
|
|
198
|
+
enabled = false
|
|
199
|
+
base_url = http://127.0.0.1:11434/v1
|
|
200
|
+
model = llama3.1
|
|
201
|
+
timeout = 60
|
|
202
|
+
max_bytes = 48000
|
|
203
|
+
notes = loop/dm devices from pytest and OOMs in capped test containers are expected
|
|
204
|
+
|
|
205
|
+
[notify]
|
|
206
|
+
enabled = false
|
|
207
|
+
ntfy_url = https://ntfy.sh/firewatcher-myhost-REPLACE_ME
|
|
208
|
+
priority = 4
|
|
209
|
+
timeout = 10
|
|
210
|
+
always_notify =
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`patterns` and `always_notify` are one path per line, or comma-separated. Positional pattern arguments replace the `patterns` list.
|
|
214
|
+
|
|
215
|
+
Out-of-range values stop `firewatcher` at startup: a negative time or count, `capture_time` over 7 days or `capture_time_max` over 30 days, `min_free_percent` outside 0–50, `purge_interval` or `resume_max` over 30 days, `buffer_max_mb` below 1, a timeout of 0 or less or over a day, an ntfy priority outside 1–5, or an LLM or ntfy URL that does not start with `http://` or `https://` or that contains spaces.
|
|
216
|
+
|
|
217
|
+
`api_key` under `[llm]` and `token` under `[notify]` are not command-line flags. `FIREWATCHER_LLM_API_KEY` and `FIREWATCHER_NTFY_TOKEN` override them when set. `--install-service` creates `/etc/firewatcher/firewatcher.conf` with mode `0600` only when it is missing, and the unit runs `firewatcher --config` that path. An existing file is left as it is. Flags passed to a later install are appended on `ExecStart` so they override the file.
|
|
218
|
+
|
|
219
|
+
Generated configs include explanatory comments above each setting. When no ntfy URL is supplied, a new config gets `https://ntfy.sh/firewatcher-<hostname>-<random>`, with a 12-character URL-safe base64 suffix (72 random bits, no padding). The hostname is sanitized and shortened as needed to keep the topic within 64 characters. Explicit URLs are preserved, and reinstalling leaves an existing config and topic unchanged. Notifications still default to off; subscribe to the generated topic in ntfy, then set `[notify] enabled = true`. When copying the example above, replace its topic with your own.
|
|
220
|
+
|
|
221
|
+
## LLM analysis and notifications
|
|
222
|
+
|
|
223
|
+
Both features default off.
|
|
224
|
+
|
|
225
|
+
- Notifications on, LLM off: every finished capture is pushed. The title is `{hostname}: {pattern}`.
|
|
226
|
+
- LLM on, notifications off: the model writes a title, summary, and notify decision. The report is appended to the capture file and logged. Nothing is pushed.
|
|
227
|
+
- Both on: a push is sent only when the model sets `notify` to true.
|
|
228
|
+
- If the LLM request times out, returns an HTTP error, or is not the expected JSON, and notifications are on, firewatcher still pushes, using the generated title.
|
|
229
|
+
|
|
230
|
+
The capture window closes even when no further log lines arrive. Analysis runs on a background worker so log following stays live. The queue holds four captures. When the queue is full, that incident skips the model and, if notifications are on, the capture thread sends the generated-title push itself.
|
|
231
|
+
|
|
232
|
+
A capture that fits in `max_bytes` (default 48000) is sent whole. A bigger one is cut around the line that opened it: the header, then the lines just before the match (a quarter of the budget), the lines just after it (half), and the end of the capture (a quarter), each cut marked `[... N bytes skipped ...]`. Room one side does not need goes to the other. The pieces are read separately, so a large capture is never loaded into memory whole.
|
|
233
|
+
|
|
234
|
+
Any user or program on the host can write log lines, and those lines reach the model. The request marks the matched line and the excerpt as untrusted data between `BEGIN`/`END` lines that carry a random token, and the system prompt tells the model never to follow instructions inside them. That makes it harder for a log line to talk the model out of notifying, but cannot rule it out. For incidents that must always reach you, list patterns under `[notify] always_notify` (or `--always-notify`): when a line that matched a watch pattern also matches one of them, the capture is pushed even if the model says not to notify. That push uses the generated title and the line that matched, followed by the model's summary, since log text can steer the summary as well. If no always_notify pattern loads, firewatcher logs an error and keeps capturing without it; `--always-notify ''` clears a list set in the config. Only the line that opened the capture and later lines that matched a watch pattern while it was open count. Other lines in the window do not, and neither do matches from an earlier capture that appear as context. The capture's color codes are removed before it is sent to the model.
|
|
235
|
+
|
|
236
|
+
`[llm] notes` (or `--llm-notes TEXT`; `note` also works as the key; the flag wins over the file, and `--llm-notes ''` clears a note set in the config) is a short free-text note about this host, for example `loop/dm devices from pytest and /tmp/claude-*/scratchpad and OOMs in capped test containers are expected`. It is sent with every capture as a line `Operator notes about this host: ...` before the untrusted log data, outside the `BEGIN`/`END` block, and the system prompt tells the model that notes describe what is normal there. Any run of whitespace in a note, newlines included, becomes one space. Only you can set it, so unlike the log lines it is trusted text; keep it short, since it is sent with every capture. When the config exists, `--install-service --llm-notes ...` puts the note on the unit's `ExecStart` (as one `--llm-notes=...` word, with `%` and `$` doubled for systemd), like any flag given there: local users can read it with `systemctl cat`, and it overrides `notes` in the config until the unit is reinstalled. To keep it private and editable, set it in the config instead.
|
|
237
|
+
|
|
238
|
+
The ntfy topic URL is not printed at startup or in error messages, since on a public ntfy server whoever knows the topic can subscribe to it. Only the server is shown. Keep the topic in the config file (mode `0600`) rather than on the command line: `--ntfy-url` shows in the process list, and when the config already exists, `--install-service --ntfy-url` puts it on the unit's `ExecStart`, which any user can read. `--install-service` warns when that happens.
|
|
239
|
+
|
|
240
|
+
Lines firewatcher writes back to the journal are a single physical line containing `firewatcher-self`, so a summary that quotes `panic` or `I/O error` does not start another capture. In journalctl and syslog output, firewatcher recognizes its own lines by the identifier field after the timestamp and host (`firewatcher[PID]:`), not by text elsewhere in the line, so a process or user name chosen to look like firewatcher cannot hide a kernel or daemon message. Lines in other formats are checked for `firewatcher-self` or `firewatcher[PID]:` anywhere. The capture files themselves stay in the log stream's original format.
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
## Development
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
pip install -r requirements-dev.txt
|
|
247
|
+
pytest
|
|
248
|
+
pytest --cov=firewatcher --cov-fail-under=95 --cov-report=term-missing
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Author
|
|
252
|
+
|
|
253
|
+
Yufei Pan (pan@zopyr.us)
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
GPL-3.0-or-later
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# firewatcher
|
|
2
|
+
|
|
3
|
+
Watch a log stream (journald, syslog, or any file) for incident patterns, then persist a window of surrounding messages to a durable location — including a network filesystem — so the record survives even if the machine later dies.
|
|
4
|
+
|
|
5
|
+
The watcher reads the journal with `journalctl --follow` when journald runs on the host (or its journal files are there, such as a host journal mounted into a container). Otherwise it follows `/var/log/syslog`, then `/var/log/messages`, even when `journalctl` is installed; `--log-file` picks a file. With no syslog file either, it falls back to `journalctl`. Running the daemon does not require systemd; only `--install-service` / `--uninstall-service` do.
|
|
6
|
+
|
|
7
|
+
Everything but [resuming after a restart](#resuming-after-a-restart) works with every source: the compressed context before a match, leaving out context already captured, the free-space floor, SIGHUP reload, and LLM analysis and pushes. Resuming needs the journal; with a log file, firewatcher reads the last `--tail-lines` lines at every start, as before.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install firewatcher
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
From a clone of this repository:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install .
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Requires **Python 3.6+**. No third-party runtime dependencies.
|
|
22
|
+
|
|
23
|
+
## Quick start
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
firewatcher /etc/firewatcher/patterns.d
|
|
27
|
+
firewatcher -t 300 -o /var/log/captured_messages/ /etc/firewatcher/patterns.d
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
A directory argument loads every non-hidden pattern file inside it. Files ending in `.regex` are compiled as regular expressions. Any other pattern file is treated as fnmatch (bash-like) substrings, with `*` added on both ends. Lines starting with `#` are comments; to match text that starts with `#`, such as the kernel's `#PF:`, write `[#]PF:` in a plain file or `\#PF:` in a `.regex` file. Matching is case-sensitive; for a case-insensitive pattern, put it in a `.regex` file with `(?i)` in front.
|
|
31
|
+
|
|
32
|
+
`firewatcher` exits with an error when no pattern loads, for example when a pattern path is mistyped or every regex is invalid, instead of running while matching nothing.
|
|
33
|
+
|
|
34
|
+
`--install-service` seeds `/etc/firewatcher/patterns.d` with example patterns when that directory is empty. The `examples/` directory in the source tree is not installed onto `PATH` by `pip install`; copy the pattern files from a clone, or let `--install-service` write them.
|
|
35
|
+
|
|
36
|
+
On a match, `firewatcher` writes:
|
|
37
|
+
|
|
38
|
+
- a per-incident capture under `{output-folder}/{YYYY-MM}/…`
|
|
39
|
+
- a line in `{output-folder}/journal.log`
|
|
40
|
+
|
|
41
|
+
Live capture is written as lines arrive, so a copy can already be on another filesystem if the host then disappears. Each captured line is flushed as it is written, and the capture file is fsynced at most once a second and when the capture closes, so a host crash loses at most about the last second of it.
|
|
42
|
+
|
|
43
|
+
A capture holds the whole `--capture-time` window before its match, so on a busy host one capture can be tens of megabytes; a lower `--capture-time` or `--buffer-max-mb` keeps captures smaller.
|
|
44
|
+
|
|
45
|
+
When a capture opens, lines of its window that an earlier capture of the same run already holds are left out, with a note naming that file and how many lines it holds. In an error storm, where each capture opens right after the last one closes, that saves writing the same context again and again; lines that no capture holds are always written. Such a capture is not complete on its own: its LLM excerpt and push see the note, not the lines in the earlier file. A capture that moved to the fallback folder, or lost lines on the way, is never pointed to: later captures write its lines again.
|
|
46
|
+
|
|
47
|
+
A capture ends `--capture-time` seconds after its last matching line, or `--capture-time-max` seconds (default 3600) after it opened, whichever comes first, so a pattern that keeps repeating still produces a finished capture, and its analysis and push, at least once an hour. A later match starts a new capture.
|
|
48
|
+
|
|
49
|
+
If the output folder cannot be written (a full disk, a read-only remount, an NFS server that returns errors), the capture continues under `--fallback-folder` (default `/var/lib/firewatcher/captured_messages/`), starting with every line written since the last successful fsync and a note naming the file it moved from. That capture is listed in the fallback folder's own `journal.log`, and the fallback folder is compressed and pruned like the output folder. If no folder can be written, `firewatcher` keeps running, logs the error, and still hands the incident to the notifier.
|
|
50
|
+
|
|
51
|
+
Put the fallback folder on a different filesystem from the output folder. The default is under `/var/lib`, which shares a disk with `/var/log` on most systems, so it covers a failed NFS output folder but not a full local disk.
|
|
52
|
+
|
|
53
|
+
The fallback needs the failure to come back as an error. On an NFS mount with the default `hard` option, writes to an unreachable server wait instead of failing, and `firewatcher` waits with them. To fall back instead, mount the output folder with `soft` (or `softerr`) and a short `timeo`.
|
|
54
|
+
|
|
55
|
+
`firewatcher` finishes the open capture on SIGINT and on SIGTERM. A second SIGTERM ends it at once.
|
|
56
|
+
|
|
57
|
+
SIGHUP reloads the pattern files and the `--always-notify` files from the paths given at startup, so after editing `/etc/firewatcher/patterns.d/`, run `sudo systemctl reload firewatcher`. The new patterns apply from the next log line, and the journal says how many loaded. If no watch pattern loads, the old ones stay and the error is logged. The config file and other settings are not re-read; restart for those. In a terminal, SIGHUP keeps its usual meaning (the terminal went away) and ends `firewatcher`. Two SIGHUPs close together reload once.
|
|
58
|
+
|
|
59
|
+
### Resuming after a restart
|
|
60
|
+
|
|
61
|
+
When firewatcher follows the journal (not a log file), it saves how far it has read in `--position-file` (default `/var/lib/firewatcher/journal.position`): about once a second while lines arrive or a capture is open, as soon as a match opens a capture, and when it stops. After a restart it reads on from there instead of the last `--tail-lines` lines; after a crash or `kill -9`, that can be up to about a second before the last line it read. Lines logged while it was stopped, such as during a reboot, are still matched, and an incident it already captured is not captured again. The `--capture-time` seconds before the saved line are read again only to refill the context kept before a match. At most `--resume-max` seconds (default 3600) are read back, so after a longer stop only the last hour is matched.
|
|
62
|
+
|
|
63
|
+
To tell where it stopped, firewatcher reads the journal with `journalctl -o short-iso`, so journal lines in captures carry the year and time zone: `2026-10-23T17:52:41+00:00 host sshd[812]: …` instead of `Oct 23 17:52:41 host sshd[812]: …`. Lines logged while firewatcher was stopped arrive all at once. A capture opened by one of them takes its context from the `--capture-time` before that line by the time each line was logged, and holds the lines after it as they are read, up to `--capture-line-count-max`.
|
|
64
|
+
|
|
65
|
+
The position is the second of the last line read, which line of that second it was, and a digest of that line. When unsure, firewatcher reads a line again rather than skip it. A run that started from `--tail-lines` in the middle of a second numbers that second's lines from where it started, so identical lines logged in that second can be captured again after a restart. A save that fails is logged once, and a restart then reads again from the last position saved. If the clock is set back after a position is saved, for example at a boot with a wrong hardware clock, new lines stamped before that second count as already read. A position more than a minute in the future is ignored.
|
|
66
|
+
|
|
67
|
+
Only one firewatcher uses a position file at a time: another one started with the same file, such as a test run in a terminal with the default settings, reads the last `--tail-lines` lines and saves nothing. An empty `--position-file` turns resuming off, and so does a journalctl that does not accept `-o short-iso` and `--since=@`, which firewatcher checks at start.
|
|
68
|
+
|
|
69
|
+
### Disk and memory limits
|
|
70
|
+
|
|
71
|
+
firewatcher keeps the filesystems it writes captures to above `--min-free-percent` free (default 1%, counted as `df` does, so the root-reserved blocks are left alone). Only what the filesystem reports authorizes a write. firewatcher checks when a capture opens, about once a second, and again before what it has written since the last check could bring it within 64 KiB of the floor (and at least every 8 MiB), fsyncing first so the filesystem, an NFS server included, counts every byte. A line that would cross that margin is not written there. Another program filling the same disk between two checks can still push it past the floor; firewatcher's own writes do not.
|
|
72
|
+
|
|
73
|
+
Near the floor, firewatcher may delete its own oldest months in that folder, so a new incident can be recorded, under these rules:
|
|
74
|
+
|
|
75
|
+
- **What:** only what firewatcher itself creates: `YYYY-MM` folders, `YYYY-MM[.N].tar.xz` archives, and their `.partial` files, each of the kind it should be (a folder, or a regular file). A month counts as a whole.
|
|
76
|
+
- **Never:** the current month, the month being written, a month with anything in it changed in the last day (another firewatcher may be writing there), a month being compressed, or a month with another filesystem mounted inside it.
|
|
77
|
+
- **How much:** only as many months as it takes, counted by their size on disk, and nothing at all if deleting every one of them still could not make room. That happens when something else filled the disk, and then the history is kept.
|
|
78
|
+
- **How often:** at most once per `--purge-interval` per folder (default an hour), so a disk that something else keeps filling drains firewatcher's history slowly. A purge that could not help is not tried again for five minutes (or the interval, if shorter). Each deletion is logged.
|
|
79
|
+
|
|
80
|
+
After deleting, firewatcher syncs and reads the free space again. A filesystem that shows freed space late costs a move to the fallback folder, never the floor. If there is still no room, the capture moves to the fallback folder with a note, or, with no folder left, stops being written, which is logged; the incident is still pushed. Compression keeps to the floor too: a month whose archive could cross it is left uncompressed for now. Purging and compression never wait for each other, so log reading never waits for `tar`. `--delete-after-months 0` means nothing is deleted by age; the floor can still delete the oldest months. Set `--min-free-percent 0` to turn all of this off.
|
|
81
|
+
|
|
82
|
+
Memory is bounded too. The `--capture-time` seconds before a match are kept in memory compressed (zlib, in blocks of about 64 KiB of text; with its bookkeeping, about 3 times smaller than the text for a busy host's varied lines, more for repetitive ones), within `--buffer-max-mb` (default 16 MiB). Past the limit the oldest block is dropped and the capture says so. The newest lines, up to 256 KiB of them before they are compressed, are always kept, so the buffer can go over the limit by about that much, or by one very long line. Compression costs well under a microsecond per line. A line longer than 1 MiB is read in pieces, and each piece is matched on its own, so a pattern split across the 1 MiB boundary is missed. The LLM excerpt reads at most `max_bytes` of a capture.
|
|
83
|
+
|
|
84
|
+
Monthly directories are compressed on a background thread, so log reading never waits for `tar`. Each month becomes `YYYY-MM.tar.xz` holding `YYYY-MM/…`. The directory is removed only after `tar` succeeds and the archive is synced to disk. An existing archive is never overwritten: the new one becomes `YYYY-MM.1.tar.xz`, and so on.
|
|
85
|
+
|
|
86
|
+
## systemd service
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
sudo firewatcher --install-service
|
|
90
|
+
sudo firewatcher --print-unit
|
|
91
|
+
sudo firewatcher --uninstall-service
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`--install-service` writes `/etc/systemd/system/firewatcher.service`, creates the output directory, seeds `/etc/firewatcher/patterns.d/` with example patterns **only if that directory is empty**, writes `/etc/firewatcher/firewatcher.conf` **only if that file is missing**, then `systemctl daemon-reload && systemctl enable --now firewatcher`.
|
|
95
|
+
|
|
96
|
+
`systemctl reload firewatcher` reloads the pattern files without a restart; see SIGHUP above.
|
|
97
|
+
|
|
98
|
+
Capture files follow the process umask. A systemd service defaults to `UMask=0022`, so captures are readable by every local user, while the journal they copy from is normally readable only by root and the `adm`/`systemd-journal` groups. On a host with untrusted local users, add `UMask=0027` to the unit, for example with `systemctl edit firewatcher`.
|
|
99
|
+
|
|
100
|
+
If systemd is not available, `--install-service` and `--uninstall-service` print a warning and exit. `--print-unit` still works so you can copy the unit elsewhere.
|
|
101
|
+
|
|
102
|
+
Useful flags:
|
|
103
|
+
|
|
104
|
+
| Flag | Description |
|
|
105
|
+
|------|-------------|
|
|
106
|
+
| `--unit-name NAME` | Unit name (default: `firewatcher`) |
|
|
107
|
+
| `--requires-mounts-for PATH` | Add `RequiresMountsFor=` (repeat for NFS/remote output) |
|
|
108
|
+
| `--no-enable` | Write the unit and reload, but do not enable or start it |
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
sudo firewatcher --install-service -o /mnt/logs/captured_messages \
|
|
112
|
+
--requires-mounts-for /mnt/logs/captured_messages \
|
|
113
|
+
/etc/firewatcher/patterns.d
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Options
|
|
117
|
+
|
|
118
|
+
| Flag | Description |
|
|
119
|
+
|------|-------------|
|
|
120
|
+
| `pattern_file …` | Pattern files or directories |
|
|
121
|
+
| `--log-file` | Log file to follow (default: the journal when journald runs, else `/var/log/syslog`, else `/var/log/messages`) |
|
|
122
|
+
| `-t`, `--capture-time` | Seconds of context after a match (default: 300) |
|
|
123
|
+
| `--capture-time-max` | Longest one capture may run, in seconds (default: 3600; 0 = no limit) |
|
|
124
|
+
| `-o`, `--output-folder` | Where to write captures (default: `/var/log/captured_messages/`) |
|
|
125
|
+
| `--fallback-folder` | Where a capture continues when the output folder cannot be written (default: `/var/lib/firewatcher/captured_messages/`; `''` disables) |
|
|
126
|
+
| `--tail-lines` | Lines to start with (default: 20; `+N` from line N, not with journalctl). Not used when the journal resumes from a saved position |
|
|
127
|
+
| `--filter-only` | Filter existing logs only (`cat` instead of `tail -F`, or `journalctl` without `--follow`). Windows are measured in read time, not log time, and existing logs are read far faster than they were written, so `-t` and `--capture-time-max` barely limit a capture: it usually runs to `--capture-line-count-max` lines or the end of the input |
|
|
128
|
+
| `--compress-after-months` | Tar.xz monthly dirs after N months (default: 3) |
|
|
129
|
+
| `--delete-after-months` | Delete `YYYY-MM` dirs after N months (default: 0 = never by age; the free-space floor can still delete the oldest months) |
|
|
130
|
+
| `--capture-line-count-max` | Max lines written after a match (default: 10000). The lines before it are bounded by `-t` and `--buffer-max-mb` |
|
|
131
|
+
| `--buffer-max-mb` | Memory, in MiB, for the lines kept before a match, compressed (default: 16). The oldest are dropped past it |
|
|
132
|
+
| `--min-free-percent` | Free-space floor, 0–50%, for the filesystems captures go to (default: 1; 0 disables). See [Disk and memory limits](#disk-and-memory-limits) |
|
|
133
|
+
| `--purge-interval SEC` | Least time between two deletions of old months in one folder for free space (default: 3600; 0: no wait) |
|
|
134
|
+
| `--position-file PATH` | Where firewatcher saves how far it read the journal, so a restart resumes there (default: `/var/lib/firewatcher/journal.position`; `''` disables). See [Resuming after a restart](#resuming-after-a-restart) |
|
|
135
|
+
| `--resume-max SEC` | Most seconds of journal read back at start: lines logged while stopped, and context before them (default: 3600; 0: none) |
|
|
136
|
+
| `--config PATH` | INI config file. Default: `/etc/firewatcher/firewatcher.conf` when that file exists. An explicit path must exist |
|
|
137
|
+
| `--llm` / `--no-llm` | Enable or disable LLM analysis of each finished capture |
|
|
138
|
+
| `--llm-base-url URL` | OpenAI-compatible API root, such as `http://127.0.0.1:11434/v1` |
|
|
139
|
+
| `--llm-model NAME` | Model name |
|
|
140
|
+
| `--llm-timeout SEC` | LLM HTTP timeout (default: 60) |
|
|
141
|
+
| `--llm-max-bytes N` | Capture bytes sent to the model (default: 48000) |
|
|
142
|
+
| `--llm-notes TEXT` | A short note about this host, sent to the model with each capture (default: none; `''` clears a note set in the config). See [LLM analysis and notifications](#llm-analysis-and-notifications) |
|
|
143
|
+
| `--notify` / `--no-notify` | Enable or disable ntfy pushes |
|
|
144
|
+
| `--ntfy-url URL` | ntfy topic URL, such as `https://ntfy.sh/mytopic` |
|
|
145
|
+
| `--ntfy-priority N` | ntfy priority 1–5 (default: 4) |
|
|
146
|
+
| `--ntfy-timeout SEC` | Worker ntfy timeout (default: 10). A full analysis queue uses 5 seconds |
|
|
147
|
+
| `--always-notify PATH` | Pattern file or directory (repeatable). A capture with a matched line that also matches one of these is pushed even when the LLM says not to notify |
|
|
148
|
+
| `--install-service` | Install and enable a systemd unit |
|
|
149
|
+
| `--print-unit` | Print that unit and exit |
|
|
150
|
+
| `--uninstall-service` | Disable and remove the unit |
|
|
151
|
+
| `-V`, `--version` | Show version and exit |
|
|
152
|
+
|
|
153
|
+
`--tail_lines`, `--filter_only`, `--no-filter_only` and `--capture_line_count_max` are still accepted with underscores.
|
|
154
|
+
|
|
155
|
+
## Config file
|
|
156
|
+
|
|
157
|
+
Runtime settings live in an INI file. Explicit command-line flags override the file. Built-in defaults apply when neither sets a value. A missing default file is ignored. `--uninstall-service` does not read the file.
|
|
158
|
+
|
|
159
|
+
A commented example with all supported settings is in [`examples/firewatcher.conf`](examples/firewatcher.conf). Copy it to `/etc/firewatcher/firewatcher.conf`, or use `firewatcher --config /path/to/firewatcher.conf`.
|
|
160
|
+
|
|
161
|
+
```ini
|
|
162
|
+
[watch]
|
|
163
|
+
patterns = /etc/firewatcher/patterns.d
|
|
164
|
+
capture_time = 300
|
|
165
|
+
capture_time_max = 3600
|
|
166
|
+
output_folder = /var/log/captured_messages/
|
|
167
|
+
fallback_folder = /var/lib/firewatcher/captured_messages/
|
|
168
|
+
filter_only = false
|
|
169
|
+
|
|
170
|
+
[llm]
|
|
171
|
+
enabled = false
|
|
172
|
+
base_url = http://127.0.0.1:11434/v1
|
|
173
|
+
model = llama3.1
|
|
174
|
+
timeout = 60
|
|
175
|
+
max_bytes = 48000
|
|
176
|
+
notes = loop/dm devices from pytest and OOMs in capped test containers are expected
|
|
177
|
+
|
|
178
|
+
[notify]
|
|
179
|
+
enabled = false
|
|
180
|
+
ntfy_url = https://ntfy.sh/firewatcher-myhost-REPLACE_ME
|
|
181
|
+
priority = 4
|
|
182
|
+
timeout = 10
|
|
183
|
+
always_notify =
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
`patterns` and `always_notify` are one path per line, or comma-separated. Positional pattern arguments replace the `patterns` list.
|
|
187
|
+
|
|
188
|
+
Out-of-range values stop `firewatcher` at startup: a negative time or count, `capture_time` over 7 days or `capture_time_max` over 30 days, `min_free_percent` outside 0–50, `purge_interval` or `resume_max` over 30 days, `buffer_max_mb` below 1, a timeout of 0 or less or over a day, an ntfy priority outside 1–5, or an LLM or ntfy URL that does not start with `http://` or `https://` or that contains spaces.
|
|
189
|
+
|
|
190
|
+
`api_key` under `[llm]` and `token` under `[notify]` are not command-line flags. `FIREWATCHER_LLM_API_KEY` and `FIREWATCHER_NTFY_TOKEN` override them when set. `--install-service` creates `/etc/firewatcher/firewatcher.conf` with mode `0600` only when it is missing, and the unit runs `firewatcher --config` that path. An existing file is left as it is. Flags passed to a later install are appended on `ExecStart` so they override the file.
|
|
191
|
+
|
|
192
|
+
Generated configs include explanatory comments above each setting. When no ntfy URL is supplied, a new config gets `https://ntfy.sh/firewatcher-<hostname>-<random>`, with a 12-character URL-safe base64 suffix (72 random bits, no padding). The hostname is sanitized and shortened as needed to keep the topic within 64 characters. Explicit URLs are preserved, and reinstalling leaves an existing config and topic unchanged. Notifications still default to off; subscribe to the generated topic in ntfy, then set `[notify] enabled = true`. When copying the example above, replace its topic with your own.
|
|
193
|
+
|
|
194
|
+
## LLM analysis and notifications
|
|
195
|
+
|
|
196
|
+
Both features default off.
|
|
197
|
+
|
|
198
|
+
- Notifications on, LLM off: every finished capture is pushed. The title is `{hostname}: {pattern}`.
|
|
199
|
+
- LLM on, notifications off: the model writes a title, summary, and notify decision. The report is appended to the capture file and logged. Nothing is pushed.
|
|
200
|
+
- Both on: a push is sent only when the model sets `notify` to true.
|
|
201
|
+
- If the LLM request times out, returns an HTTP error, or is not the expected JSON, and notifications are on, firewatcher still pushes, using the generated title.
|
|
202
|
+
|
|
203
|
+
The capture window closes even when no further log lines arrive. Analysis runs on a background worker so log following stays live. The queue holds four captures. When the queue is full, that incident skips the model and, if notifications are on, the capture thread sends the generated-title push itself.
|
|
204
|
+
|
|
205
|
+
A capture that fits in `max_bytes` (default 48000) is sent whole. A bigger one is cut around the line that opened it: the header, then the lines just before the match (a quarter of the budget), the lines just after it (half), and the end of the capture (a quarter), each cut marked `[... N bytes skipped ...]`. Room one side does not need goes to the other. The pieces are read separately, so a large capture is never loaded into memory whole.
|
|
206
|
+
|
|
207
|
+
Any user or program on the host can write log lines, and those lines reach the model. The request marks the matched line and the excerpt as untrusted data between `BEGIN`/`END` lines that carry a random token, and the system prompt tells the model never to follow instructions inside them. That makes it harder for a log line to talk the model out of notifying, but cannot rule it out. For incidents that must always reach you, list patterns under `[notify] always_notify` (or `--always-notify`): when a line that matched a watch pattern also matches one of them, the capture is pushed even if the model says not to notify. That push uses the generated title and the line that matched, followed by the model's summary, since log text can steer the summary as well. If no always_notify pattern loads, firewatcher logs an error and keeps capturing without it; `--always-notify ''` clears a list set in the config. Only the line that opened the capture and later lines that matched a watch pattern while it was open count. Other lines in the window do not, and neither do matches from an earlier capture that appear as context. The capture's color codes are removed before it is sent to the model.
|
|
208
|
+
|
|
209
|
+
`[llm] notes` (or `--llm-notes TEXT`; `note` also works as the key; the flag wins over the file, and `--llm-notes ''` clears a note set in the config) is a short free-text note about this host, for example `loop/dm devices from pytest and /tmp/claude-*/scratchpad and OOMs in capped test containers are expected`. It is sent with every capture as a line `Operator notes about this host: ...` before the untrusted log data, outside the `BEGIN`/`END` block, and the system prompt tells the model that notes describe what is normal there. Any run of whitespace in a note, newlines included, becomes one space. Only you can set it, so unlike the log lines it is trusted text; keep it short, since it is sent with every capture. When the config exists, `--install-service --llm-notes ...` puts the note on the unit's `ExecStart` (as one `--llm-notes=...` word, with `%` and `$` doubled for systemd), like any flag given there: local users can read it with `systemctl cat`, and it overrides `notes` in the config until the unit is reinstalled. To keep it private and editable, set it in the config instead.
|
|
210
|
+
|
|
211
|
+
The ntfy topic URL is not printed at startup or in error messages, since on a public ntfy server whoever knows the topic can subscribe to it. Only the server is shown. Keep the topic in the config file (mode `0600`) rather than on the command line: `--ntfy-url` shows in the process list, and when the config already exists, `--install-service --ntfy-url` puts it on the unit's `ExecStart`, which any user can read. `--install-service` warns when that happens.
|
|
212
|
+
|
|
213
|
+
Lines firewatcher writes back to the journal are a single physical line containing `firewatcher-self`, so a summary that quotes `panic` or `I/O error` does not start another capture. In journalctl and syslog output, firewatcher recognizes its own lines by the identifier field after the timestamp and host (`firewatcher[PID]:`), not by text elsewhere in the line, so a process or user name chosen to look like firewatcher cannot hide a kernel or daemon message. Lines in other formats are checked for `firewatcher-self` or `firewatcher[PID]:` anywhere. The capture files themselves stay in the log stream's original format.
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
## Development
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
pip install -r requirements-dev.txt
|
|
220
|
+
pytest
|
|
221
|
+
pytest --cov=firewatcher --cov-fail-under=95 --cov-report=term-missing
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Author
|
|
225
|
+
|
|
226
|
+
Yufei Pan (pan@zopyr.us)
|
|
227
|
+
|
|
228
|
+
## License
|
|
229
|
+
|
|
230
|
+
GPL-3.0-or-later
|
|
@@ -10,8 +10,12 @@ patterns = /etc/firewatcher/patterns.d
|
|
|
10
10
|
log_file =
|
|
11
11
|
# Seconds of context to capture after a match.
|
|
12
12
|
capture_time = 300
|
|
13
|
+
# Longest one capture may run, in seconds, however often its pattern repeats; 0 disables.
|
|
14
|
+
capture_time_max = 3600
|
|
13
15
|
# Directory for incident captures and journal.log.
|
|
14
16
|
output_folder = /var/log/captured_messages/
|
|
17
|
+
# Where a capture continues when output_folder cannot be written; leave blank to disable.
|
|
18
|
+
fallback_folder = /var/lib/firewatcher/captured_messages/
|
|
15
19
|
# Initial lines to read; +N starts at line N when reading a file.
|
|
16
20
|
tail_lines = 20
|
|
17
21
|
# Process existing logs only instead of following new messages.
|
|
@@ -20,8 +24,20 @@ filter_only = false
|
|
|
20
24
|
compress_after_months = 3
|
|
21
25
|
# Delete monthly capture directories after this many months; 0 disables.
|
|
22
26
|
delete_after_months = 0
|
|
23
|
-
# Maximum
|
|
27
|
+
# Maximum lines written after a match; the lines before it are bounded by capture_time and buffer_max_mb.
|
|
24
28
|
capture_line_count_max = 10000
|
|
29
|
+
# Memory, in MiB, for the lines kept before a match, compressed; the oldest are dropped past it.
|
|
30
|
+
buffer_max_mb = 16
|
|
31
|
+
# Free-space floor in percent (0 to 50; 0 disables). Near it, old months may be deleted (see the README),
|
|
32
|
+
# then captures move to the fallback folder or stop.
|
|
33
|
+
min_free_percent = 1
|
|
34
|
+
# Least seconds between two deletions of old months in one folder for free space; 0: no wait.
|
|
35
|
+
purge_interval = 3600
|
|
36
|
+
# Where firewatcher remembers how far it read the journal, so a restart resumes there;
|
|
37
|
+
# leave blank to read the last tail_lines lines at every start instead.
|
|
38
|
+
position_file = /var/lib/firewatcher/journal.position
|
|
39
|
+
# At start, the most seconds of journal read back: lines logged while stopped, then context; 0: none.
|
|
40
|
+
resume_max = 3600
|
|
25
41
|
|
|
26
42
|
[llm]
|
|
27
43
|
# Set true after configuring your OpenAI-compatible API and model.
|
|
@@ -36,6 +52,8 @@ api_key =
|
|
|
36
52
|
timeout = 60
|
|
37
53
|
# Maximum capture bytes sent to the model.
|
|
38
54
|
max_bytes = 48000
|
|
55
|
+
# Optional note sent to the model with each capture, e.g. what is normal on this host.
|
|
56
|
+
notes =
|
|
39
57
|
|
|
40
58
|
[notify]
|
|
41
59
|
# Enable pushes; when LLM analysis is on, the model decides whether to notify.
|
|
@@ -50,3 +68,6 @@ token =
|
|
|
50
68
|
priority = 4
|
|
51
69
|
# HTTP timeout in seconds.
|
|
52
70
|
timeout = 10
|
|
71
|
+
# Pattern files or directories. A capture whose matched lines match one of these is pushed
|
|
72
|
+
# even when the LLM says not to notify. Blank: the LLM decides for every capture.
|
|
73
|
+
always_notify =
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
hard resetting link
|
|
2
2
|
Initializing cgroup subsys cpuset
|
|
3
3
|
Linux version
|
|
4
|
-
Out of
|
|
4
|
+
Out of memory
|
|
5
|
+
out of memory: Killed process
|
|
5
6
|
Call Trace
|
|
6
7
|
I/O error
|
|
7
8
|
bad sector
|
|
@@ -13,3 +14,4 @@ timeout, reset controller
|
|
|
13
14
|
timeout, disable controller
|
|
14
15
|
iotest: 1% high is too high compared to 1% low!
|
|
15
16
|
Hardware error
|
|
17
|
+
Hardware Error
|