syslog-postmortem 1.0.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.
- syslog_postmortem-1.0.0/LICENSE +21 -0
- syslog_postmortem-1.0.0/PKG-INFO +149 -0
- syslog_postmortem-1.0.0/README.md +129 -0
- syslog_postmortem-1.0.0/pyproject.toml +32 -0
- syslog_postmortem-1.0.0/setup.cfg +4 -0
- syslog_postmortem-1.0.0/syslog_postmortem/__init__.py +4 -0
- syslog_postmortem-1.0.0/syslog_postmortem/analyzer.py +231 -0
- syslog_postmortem-1.0.0/syslog_postmortem/cli.py +157 -0
- syslog_postmortem-1.0.0/syslog_postmortem/collector.py +228 -0
- syslog_postmortem-1.0.0/syslog_postmortem/patterns.py +108 -0
- syslog_postmortem-1.0.0/syslog_postmortem/renderer.py +284 -0
- syslog_postmortem-1.0.0/syslog_postmortem.egg-info/PKG-INFO +149 -0
- syslog_postmortem-1.0.0/syslog_postmortem.egg-info/SOURCES.txt +15 -0
- syslog_postmortem-1.0.0/syslog_postmortem.egg-info/dependency_links.txt +1 -0
- syslog_postmortem-1.0.0/syslog_postmortem.egg-info/entry_points.txt +2 -0
- syslog_postmortem-1.0.0/syslog_postmortem.egg-info/requires.txt +1 -0
- syslog_postmortem-1.0.0/syslog_postmortem.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Serber1990
|
|
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,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: syslog-postmortem
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Generate structured postmortem drafts from journalctl, syslog, auth.log and dmesg
|
|
5
|
+
Author-email: Serber1990 <serber1990@pm.me>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/serber1990/syslog-postmortem
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/serber1990/syslog-postmortem/issues
|
|
9
|
+
Project-URL: Source Code, https://github.com/serber1990/syslog-postmortem
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Topic :: System :: Logging
|
|
14
|
+
Classifier: Topic :: System :: Systems Administration
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: shellcolorize
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
# syslog-postmortem
|
|
22
|
+
|
|
23
|
+
[](https://badge.fury.io/py/syslog-postmortem)
|
|
24
|
+
[](https://opensource.org/licenses/MIT)
|
|
25
|
+
|
|
26
|
+
Generate a **structured postmortem draft** from system logs in seconds — no more reconstructing incident timelines by hand.
|
|
27
|
+
|
|
28
|
+
Pulls from `journalctl`, `/var/log/syslog`, `/var/log/auth.log` and `/var/log/dmesg`, correlates events, detects patterns, and produces a ready-to-edit Markdown or HTML postmortem.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## ✨ What it does
|
|
33
|
+
|
|
34
|
+
Given a time window and optional service list, `syslog-postmortem`:
|
|
35
|
+
|
|
36
|
+
1. **Collects** entries from `journalctl` (primary) and `/var/log` files (supplement)
|
|
37
|
+
2. **Deduplicates** repeated messages and counts occurrences
|
|
38
|
+
3. **Detects patterns** — OOM kills, service crashes, auth bursts, disk exhaustion, cascading failures, connection errors
|
|
39
|
+
4. **Builds a timeline** sorted chronologically with severity icons
|
|
40
|
+
5. **Identifies contributing factors** automatically (restart loops, cascade chains, error bursts)
|
|
41
|
+
6. **Generates action items** from pattern hints
|
|
42
|
+
7. **Outputs** a complete Markdown postmortem ready to edit and share
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 📥 Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
pip install syslog-postmortem
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 🛠 Usage
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
postmortem --from "2026-05-10 14:00" --to "2026-05-10 16:00"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
# Focus on specific services
|
|
62
|
+
postmortem --from "2026-05-10 14:00" --to "2026-05-10 16:00" \
|
|
63
|
+
--services nginx,postgresql,redis \
|
|
64
|
+
--title "Database outage" \
|
|
65
|
+
--output incident-2026-05-10.md
|
|
66
|
+
|
|
67
|
+
# HTML output
|
|
68
|
+
postmortem --from "2026-05-10 14:00" --to "2026-05-10 16:00" \
|
|
69
|
+
--format html --output report.html
|
|
70
|
+
|
|
71
|
+
# journalctl only (skip /var/log files)
|
|
72
|
+
postmortem --from "2026-05-10 14:00" --to "2026-05-10 16:00" --no-files
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## 📋 Options
|
|
78
|
+
|
|
79
|
+
| Option | Description |
|
|
80
|
+
|--------|-------------|
|
|
81
|
+
| `--from DATETIME` | Start of incident window **required** |
|
|
82
|
+
| `--to DATETIME` | End of incident window **required** |
|
|
83
|
+
| `--title TEXT` | Postmortem title (default: `Incident YYYY-MM-DD`) |
|
|
84
|
+
| `--services LIST` | Comma-separated services to focus on |
|
|
85
|
+
| `--output FILE` | Output path (default: `postmortem_YYYYMMDD_HHMM.md`) |
|
|
86
|
+
| `--format` | `markdown` (default) or `html` |
|
|
87
|
+
| `--no-files` | Skip `/var/log` parsing, use journalctl only |
|
|
88
|
+
| `--priorities` | journalctl priority filter (default: `0..4`) |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 📄 Output structure
|
|
93
|
+
|
|
94
|
+
```markdown
|
|
95
|
+
# Postmortem: Database outage
|
|
96
|
+
|
|
97
|
+
| | |
|
|
98
|
+
|---|---|
|
|
99
|
+
| **Date** | 2026-05-10 |
|
|
100
|
+
| **Window** | 2026-05-10 14:00 → 2026-05-10 16:00 |
|
|
101
|
+
| **Duration** | 2h 0m |
|
|
102
|
+
| **Severity** | Critical |
|
|
103
|
+
|
|
104
|
+
## Summary
|
|
105
|
+
Analysis of 1,243 raw log entries (89 unique events after deduplication)...
|
|
106
|
+
First anomaly detected at **14:03:22** in **postgresql** (CRITICAL).
|
|
107
|
+
|
|
108
|
+
## Timeline
|
|
109
|
+
| Time | Service | Severity | Event |
|
|
110
|
+
|------|---------|----------|-------|
|
|
111
|
+
| 14:03:22 | `postgresql` | ⛔ CRITICAL | could not connect to server |
|
|
112
|
+
| 14:03:45 | `nginx` | 🔴 ERROR | upstream connect error |
|
|
113
|
+
|
|
114
|
+
## Contributing Factors
|
|
115
|
+
- **Service instability**: `postgresql` triggered restart-loop detection 4 time(s)
|
|
116
|
+
- **Cascading failure**: `nginx` errors began 23s after first `postgresql` critical event
|
|
117
|
+
|
|
118
|
+
## Action Items
|
|
119
|
+
- [ ] Investigate postgresql restart cause; check dependencies and configuration
|
|
120
|
+
- [ ] Verify the downstream service is running and listening on the expected port
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 🔍 Detected patterns
|
|
126
|
+
|
|
127
|
+
| Pattern | Triggers |
|
|
128
|
+
|---------|----------|
|
|
129
|
+
| OOM Killer | `out of memory`, `oom_kill`, `killed process` |
|
|
130
|
+
| Disk Full | `no space left on device`, `disk full` |
|
|
131
|
+
| Kernel Error | `kernel: BUG`, `segfault`, `general protection` |
|
|
132
|
+
| Service Failed | `failed to start`, `unit entered failed state` |
|
|
133
|
+
| Process Crash | `segmentation fault`, `core dump`, `aborted` |
|
|
134
|
+
| Connection Refused | `connection refused`, `upstream connect error` |
|
|
135
|
+
| Timeout | `timed out`, `ETIMEDOUT`, `request timeout` |
|
|
136
|
+
| Auth Failure | `Failed password`, `authentication failure`, `invalid user` |
|
|
137
|
+
| SSL/TLS Error | `certificate expired`, `TLS handshake failed` |
|
|
138
|
+
| Database Error | `could not connect to database`, `max connections reached` |
|
|
139
|
+
| High Load | `load average` spike, `cpu throttled` |
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 📝 License
|
|
144
|
+
|
|
145
|
+
MIT — see [LICENSE](LICENSE).
|
|
146
|
+
|
|
147
|
+
## 🌐 Connect
|
|
148
|
+
|
|
149
|
+
[](https://github.com/serber1990)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# syslog-postmortem
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/py/syslog-postmortem)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
Generate a **structured postmortem draft** from system logs in seconds — no more reconstructing incident timelines by hand.
|
|
7
|
+
|
|
8
|
+
Pulls from `journalctl`, `/var/log/syslog`, `/var/log/auth.log` and `/var/log/dmesg`, correlates events, detects patterns, and produces a ready-to-edit Markdown or HTML postmortem.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## ✨ What it does
|
|
13
|
+
|
|
14
|
+
Given a time window and optional service list, `syslog-postmortem`:
|
|
15
|
+
|
|
16
|
+
1. **Collects** entries from `journalctl` (primary) and `/var/log` files (supplement)
|
|
17
|
+
2. **Deduplicates** repeated messages and counts occurrences
|
|
18
|
+
3. **Detects patterns** — OOM kills, service crashes, auth bursts, disk exhaustion, cascading failures, connection errors
|
|
19
|
+
4. **Builds a timeline** sorted chronologically with severity icons
|
|
20
|
+
5. **Identifies contributing factors** automatically (restart loops, cascade chains, error bursts)
|
|
21
|
+
6. **Generates action items** from pattern hints
|
|
22
|
+
7. **Outputs** a complete Markdown postmortem ready to edit and share
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 📥 Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install syslog-postmortem
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 🛠 Usage
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
postmortem --from "2026-05-10 14:00" --to "2026-05-10 16:00"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
# Focus on specific services
|
|
42
|
+
postmortem --from "2026-05-10 14:00" --to "2026-05-10 16:00" \
|
|
43
|
+
--services nginx,postgresql,redis \
|
|
44
|
+
--title "Database outage" \
|
|
45
|
+
--output incident-2026-05-10.md
|
|
46
|
+
|
|
47
|
+
# HTML output
|
|
48
|
+
postmortem --from "2026-05-10 14:00" --to "2026-05-10 16:00" \
|
|
49
|
+
--format html --output report.html
|
|
50
|
+
|
|
51
|
+
# journalctl only (skip /var/log files)
|
|
52
|
+
postmortem --from "2026-05-10 14:00" --to "2026-05-10 16:00" --no-files
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## 📋 Options
|
|
58
|
+
|
|
59
|
+
| Option | Description |
|
|
60
|
+
|--------|-------------|
|
|
61
|
+
| `--from DATETIME` | Start of incident window **required** |
|
|
62
|
+
| `--to DATETIME` | End of incident window **required** |
|
|
63
|
+
| `--title TEXT` | Postmortem title (default: `Incident YYYY-MM-DD`) |
|
|
64
|
+
| `--services LIST` | Comma-separated services to focus on |
|
|
65
|
+
| `--output FILE` | Output path (default: `postmortem_YYYYMMDD_HHMM.md`) |
|
|
66
|
+
| `--format` | `markdown` (default) or `html` |
|
|
67
|
+
| `--no-files` | Skip `/var/log` parsing, use journalctl only |
|
|
68
|
+
| `--priorities` | journalctl priority filter (default: `0..4`) |
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 📄 Output structure
|
|
73
|
+
|
|
74
|
+
```markdown
|
|
75
|
+
# Postmortem: Database outage
|
|
76
|
+
|
|
77
|
+
| | |
|
|
78
|
+
|---|---|
|
|
79
|
+
| **Date** | 2026-05-10 |
|
|
80
|
+
| **Window** | 2026-05-10 14:00 → 2026-05-10 16:00 |
|
|
81
|
+
| **Duration** | 2h 0m |
|
|
82
|
+
| **Severity** | Critical |
|
|
83
|
+
|
|
84
|
+
## Summary
|
|
85
|
+
Analysis of 1,243 raw log entries (89 unique events after deduplication)...
|
|
86
|
+
First anomaly detected at **14:03:22** in **postgresql** (CRITICAL).
|
|
87
|
+
|
|
88
|
+
## Timeline
|
|
89
|
+
| Time | Service | Severity | Event |
|
|
90
|
+
|------|---------|----------|-------|
|
|
91
|
+
| 14:03:22 | `postgresql` | ⛔ CRITICAL | could not connect to server |
|
|
92
|
+
| 14:03:45 | `nginx` | 🔴 ERROR | upstream connect error |
|
|
93
|
+
|
|
94
|
+
## Contributing Factors
|
|
95
|
+
- **Service instability**: `postgresql` triggered restart-loop detection 4 time(s)
|
|
96
|
+
- **Cascading failure**: `nginx` errors began 23s after first `postgresql` critical event
|
|
97
|
+
|
|
98
|
+
## Action Items
|
|
99
|
+
- [ ] Investigate postgresql restart cause; check dependencies and configuration
|
|
100
|
+
- [ ] Verify the downstream service is running and listening on the expected port
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## 🔍 Detected patterns
|
|
106
|
+
|
|
107
|
+
| Pattern | Triggers |
|
|
108
|
+
|---------|----------|
|
|
109
|
+
| OOM Killer | `out of memory`, `oom_kill`, `killed process` |
|
|
110
|
+
| Disk Full | `no space left on device`, `disk full` |
|
|
111
|
+
| Kernel Error | `kernel: BUG`, `segfault`, `general protection` |
|
|
112
|
+
| Service Failed | `failed to start`, `unit entered failed state` |
|
|
113
|
+
| Process Crash | `segmentation fault`, `core dump`, `aborted` |
|
|
114
|
+
| Connection Refused | `connection refused`, `upstream connect error` |
|
|
115
|
+
| Timeout | `timed out`, `ETIMEDOUT`, `request timeout` |
|
|
116
|
+
| Auth Failure | `Failed password`, `authentication failure`, `invalid user` |
|
|
117
|
+
| SSL/TLS Error | `certificate expired`, `TLS handshake failed` |
|
|
118
|
+
| Database Error | `could not connect to database`, `max connections reached` |
|
|
119
|
+
| High Load | `load average` spike, `cpu throttled` |
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## 📝 License
|
|
124
|
+
|
|
125
|
+
MIT — see [LICENSE](LICENSE).
|
|
126
|
+
|
|
127
|
+
## 🌐 Connect
|
|
128
|
+
|
|
129
|
+
[](https://github.com/serber1990)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "syslog-postmortem"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Generate structured postmortem drafts from journalctl, syslog, auth.log and dmesg"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
authors = [{ name = "Serber1990", email = "serber1990@pm.me" }]
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
dependencies = ["shellcolorize"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
"Environment :: Console",
|
|
18
|
+
"Topic :: System :: Logging",
|
|
19
|
+
"Topic :: System :: Systems Administration",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/serber1990/syslog-postmortem"
|
|
24
|
+
"Bug Tracker" = "https://github.com/serber1990/syslog-postmortem/issues"
|
|
25
|
+
"Source Code" = "https://github.com/serber1990/syslog-postmortem"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
postmortem = "syslog_postmortem.cli:main"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools.packages.find]
|
|
31
|
+
where = ["."]
|
|
32
|
+
include = ["syslog_postmortem*"]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pattern analysis, deduplication, cascade detection and contributing factor generation.
|
|
3
|
+
"""
|
|
4
|
+
from collections import Counter, defaultdict
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
from typing import List, Dict, Optional
|
|
8
|
+
|
|
9
|
+
from .collector import RawEntry, priority_to_severity
|
|
10
|
+
from .patterns import match_patterns, Pattern
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ── Normalised event ──────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class Event:
|
|
17
|
+
timestamp: datetime
|
|
18
|
+
service: str
|
|
19
|
+
severity: str # CRITICAL | ERROR | WARNING | INFO
|
|
20
|
+
message: str
|
|
21
|
+
source: str
|
|
22
|
+
patterns: List[Pattern] = field(default_factory=list)
|
|
23
|
+
count: int = 1 # after deduplication
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ── Analysis result ───────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class AnalysisResult:
|
|
30
|
+
events: List[Event]
|
|
31
|
+
timeline: List[Event] # deduplicated, sorted
|
|
32
|
+
by_severity: Dict[str, List[Event]]
|
|
33
|
+
by_service: Dict[str, List[Event]]
|
|
34
|
+
pattern_counts: Dict[str, int] # pattern_name → count
|
|
35
|
+
contributing_factors: List[str]
|
|
36
|
+
action_items: List[str]
|
|
37
|
+
first_anomaly: Optional[Event]
|
|
38
|
+
peak_window: Optional[datetime] # minute with most errors
|
|
39
|
+
services_affected: List[str]
|
|
40
|
+
total_raw: int
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ── Helpers ───────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
def _deduplicate(events: List[Event], window_seconds: int = 60) -> List[Event]:
|
|
46
|
+
"""
|
|
47
|
+
Merge identical messages that repeat within `window_seconds`.
|
|
48
|
+
Keeps the first occurrence with a count of how many times it appeared.
|
|
49
|
+
"""
|
|
50
|
+
result: List[Event] = []
|
|
51
|
+
seen: Dict[str, Event] = {}
|
|
52
|
+
|
|
53
|
+
for ev in events:
|
|
54
|
+
key = (ev.service, ev.message[:120])
|
|
55
|
+
if key in seen:
|
|
56
|
+
last = seen[key]
|
|
57
|
+
if (ev.timestamp - last.timestamp).total_seconds() <= window_seconds:
|
|
58
|
+
last.count += 1
|
|
59
|
+
continue
|
|
60
|
+
seen[key] = ev
|
|
61
|
+
result.append(ev)
|
|
62
|
+
|
|
63
|
+
return result
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _peak_minute(events: List[Event]) -> Optional[datetime]:
|
|
67
|
+
buckets: Counter = Counter()
|
|
68
|
+
for ev in events:
|
|
69
|
+
if ev.severity in ('CRITICAL', 'ERROR', 'WARNING'):
|
|
70
|
+
buckets[ev.timestamp.replace(second=0, microsecond=0)] += 1
|
|
71
|
+
return buckets.most_common(1)[0][0] if buckets else None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _detect_cascades(events: List[Event], window_seconds: int = 120) -> List[str]:
|
|
75
|
+
"""
|
|
76
|
+
Detect cascading failures: service A fails, service B fails shortly after.
|
|
77
|
+
Returns human-readable descriptions.
|
|
78
|
+
"""
|
|
79
|
+
cascades = []
|
|
80
|
+
criticals = [e for e in events if e.severity == 'CRITICAL']
|
|
81
|
+
errors = [e for e in events if e.severity == 'ERROR']
|
|
82
|
+
|
|
83
|
+
for trigger in criticals:
|
|
84
|
+
for follow in errors:
|
|
85
|
+
if follow.service == trigger.service:
|
|
86
|
+
continue
|
|
87
|
+
delta = (follow.timestamp - trigger.timestamp).total_seconds()
|
|
88
|
+
if 0 < delta <= window_seconds:
|
|
89
|
+
cascades.append(
|
|
90
|
+
f"**{follow.service}** errors began "
|
|
91
|
+
f"**{int(delta)}s** after first **{trigger.service}** critical event "
|
|
92
|
+
f"({trigger.timestamp.strftime('%H:%M:%S')})"
|
|
93
|
+
)
|
|
94
|
+
break # one cascade per trigger service pair
|
|
95
|
+
|
|
96
|
+
return cascades
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# ── Main analyser ─────────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
def analyze(raw: List[RawEntry], services_filter: List[str] = None) -> AnalysisResult:
|
|
102
|
+
total_raw = len(raw)
|
|
103
|
+
|
|
104
|
+
# 1. Normalise RawEntry → Event and run pattern matching
|
|
105
|
+
events: List[Event] = []
|
|
106
|
+
for r in raw:
|
|
107
|
+
if services_filter and r.service.lower() not in [s.lower() for s in services_filter]:
|
|
108
|
+
continue
|
|
109
|
+
severity = priority_to_severity(r.priority)
|
|
110
|
+
matched = match_patterns(r.message)
|
|
111
|
+
# Upgrade severity based on pattern if stricter
|
|
112
|
+
sev_rank = {'CRITICAL': 0, 'ERROR': 1, 'WARNING': 2, 'INFO': 3, 'DEBUG': 4}
|
|
113
|
+
for p in matched:
|
|
114
|
+
if sev_rank.get(p.severity, 4) < sev_rank.get(severity, 4):
|
|
115
|
+
severity = p.severity
|
|
116
|
+
events.append(Event(
|
|
117
|
+
timestamp=r.timestamp,
|
|
118
|
+
service=r.service,
|
|
119
|
+
severity=severity,
|
|
120
|
+
message=r.message,
|
|
121
|
+
source=r.source,
|
|
122
|
+
patterns=matched,
|
|
123
|
+
))
|
|
124
|
+
|
|
125
|
+
# 2. Deduplicate
|
|
126
|
+
timeline = _deduplicate(events)
|
|
127
|
+
|
|
128
|
+
# 3. Group by severity and service
|
|
129
|
+
by_severity: Dict[str, List[Event]] = defaultdict(list)
|
|
130
|
+
by_service: Dict[str, List[Event]] = defaultdict(list)
|
|
131
|
+
for ev in timeline:
|
|
132
|
+
by_severity[ev.severity].append(ev)
|
|
133
|
+
by_service[ev.service].append(ev)
|
|
134
|
+
|
|
135
|
+
# 4. Pattern counts
|
|
136
|
+
pattern_counts: Counter = Counter()
|
|
137
|
+
for ev in timeline:
|
|
138
|
+
for p in ev.patterns:
|
|
139
|
+
pattern_counts[p.name] += ev.count
|
|
140
|
+
|
|
141
|
+
# 5. First anomaly (first CRITICAL or ERROR)
|
|
142
|
+
first_anomaly = next(
|
|
143
|
+
(e for e in timeline if e.severity in ('CRITICAL', 'ERROR')), None
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# 6. Peak minute
|
|
147
|
+
peak_window = _peak_minute(timeline)
|
|
148
|
+
|
|
149
|
+
# 7. Services affected (those with at least one ERROR+)
|
|
150
|
+
services_affected = sorted({
|
|
151
|
+
e.service for e in timeline
|
|
152
|
+
if e.severity in ('CRITICAL', 'ERROR')
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
# 8. Contributing factors
|
|
156
|
+
factors: List[str] = []
|
|
157
|
+
|
|
158
|
+
# Restart loops
|
|
159
|
+
restart_counts = Counter(
|
|
160
|
+
e.service for e in timeline
|
|
161
|
+
if any(p.name == 'service_restart' for p in e.patterns)
|
|
162
|
+
)
|
|
163
|
+
for svc, cnt in restart_counts.most_common():
|
|
164
|
+
factors.append(
|
|
165
|
+
f"**Service instability**: `{svc}` triggered restart-loop detection **{cnt} time(s)**"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# OOM events
|
|
169
|
+
oom_count = pattern_counts.get('oom', 0)
|
|
170
|
+
if oom_count:
|
|
171
|
+
factors.append(
|
|
172
|
+
f"**Memory pressure**: OOM killer fired **{oom_count} time(s)** during the window"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Disk full
|
|
176
|
+
if pattern_counts.get('disk_full', 0):
|
|
177
|
+
factors.append("**Disk exhaustion**: 'No space left on device' events detected")
|
|
178
|
+
|
|
179
|
+
# Auth failure burst (>5 in window)
|
|
180
|
+
auth_count = sum(
|
|
181
|
+
e.count for e in timeline
|
|
182
|
+
if any(p.name == 'auth_failure' for p in e.patterns)
|
|
183
|
+
)
|
|
184
|
+
if auth_count >= 5:
|
|
185
|
+
factors.append(
|
|
186
|
+
f"**Auth anomaly**: Burst of **{auth_count} authentication failure(s)** detected"
|
|
187
|
+
" — possible brute-force attempt"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Cascading failures
|
|
191
|
+
factors.extend(_detect_cascades(timeline))
|
|
192
|
+
|
|
193
|
+
# High error density (>30 error events in any 5-minute window)
|
|
194
|
+
buckets: Counter = Counter()
|
|
195
|
+
for ev in timeline:
|
|
196
|
+
if ev.severity in ('CRITICAL', 'ERROR'):
|
|
197
|
+
slot = ev.timestamp.replace(second=0, microsecond=0)
|
|
198
|
+
slot = slot.replace(minute=(slot.minute // 5) * 5)
|
|
199
|
+
buckets[slot] += ev.count
|
|
200
|
+
dense = [(t, c) for t, c in buckets.items() if c >= 30]
|
|
201
|
+
for t, c in sorted(dense):
|
|
202
|
+
factors.append(
|
|
203
|
+
f"**Error burst**: **{c} errors** in the 5-minute window starting "
|
|
204
|
+
f"{t.strftime('%H:%M')}"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if not factors:
|
|
208
|
+
factors.append("No specific contributing factors auto-detected — manual analysis required")
|
|
209
|
+
|
|
210
|
+
# 9. Action items (deduplicated from pattern hints)
|
|
211
|
+
seen_hints: set = set()
|
|
212
|
+
action_items: List[str] = []
|
|
213
|
+
for ev in timeline:
|
|
214
|
+
for p in ev.patterns:
|
|
215
|
+
if p.action_hint not in seen_hints:
|
|
216
|
+
seen_hints.add(p.action_hint)
|
|
217
|
+
action_items.append(p.action_hint)
|
|
218
|
+
|
|
219
|
+
return AnalysisResult(
|
|
220
|
+
events=events,
|
|
221
|
+
timeline=timeline,
|
|
222
|
+
by_severity=dict(by_severity),
|
|
223
|
+
by_service=dict(by_service),
|
|
224
|
+
pattern_counts=dict(pattern_counts),
|
|
225
|
+
contributing_factors=factors,
|
|
226
|
+
action_items=action_items,
|
|
227
|
+
first_anomaly=first_anomaly,
|
|
228
|
+
peak_window=peak_window,
|
|
229
|
+
services_affected=services_affected,
|
|
230
|
+
total_raw=total_raw,
|
|
231
|
+
)
|