7 Commits
7 changed files with 128 additions and 46 deletions
+15 -10
View File
@@ -1,6 +1,6 @@
# LOGAR: Edge-Thin Log Analysis & Temporal Verification System # LOGAR: Edge-Thin Log Analysis & Temporal Verification System
**LOGAR** is an enterprise log aggregation, verification, and anomaly detection architecture designed for heterogeneous server fleets (Windows & Linux). It combines lightweight zero-state edge forwarders with a centralized cloud hub that applies OpenPGP encryption, authenticated TCP streaming, temporal persistence tracking across 12-hour evaluation windows, and an automated 4-run rule to filter out transient infrastructure blips before reporting verified anomalies to **Hermes**. **LOGAR** is an enterprise log aggregation, verification, and anomaly detection architecture designed for heterogeneous server fleets (Windows & Linux). It combines lightweight zero-state edge forwarders with a centralized cloud hub that applies OpenPGP encryption, authenticated TCP streaming, temporal persistence tracking across 12-hour evaluation windows, an automated 4-run rule to filter transient warnings, and immediate pass-through for critical errors before reporting verified anomalies to **Hermes**.
--- ---
@@ -27,14 +27,15 @@ Site agents running on Windows and Linux act strictly as lightweight forwarders:
- **End-to-End Encryption**: Logs are encrypted using the server's OpenPGP public key before leaving the edge node. - **End-to-End Encryption**: Logs are encrypted using the server's OpenPGP public key before leaving the edge node.
- **Secure TCP Sockets**: Ingestion occurs over low-overhead authenticated TCP sockets rather than bulky HTTP/HTTPS endpoints. - **Secure TCP Sockets**: Ingestion occurs over low-overhead authenticated TCP sockets rather than bulky HTTP/HTTPS endpoints.
### 2. Cloud-Side Temporal Persistence ### 2. Cloud-Side Temporal Persistence & Severity Routing
The central Python/TCP hub handles the heavy lifting: The central Python/TCP hub handles the heavy lifting:
- State tracking is managed centrally in SQLite (`logar_state.db`). - State tracking is managed centrally in SQLite (`logar_state.db`).
- Candidate issues are evaluated over a **12-hour temporal evaluation window**. - Candidate issues are evaluated over a **12-hour temporal evaluation window**.
- An issue must persist across **at least 4 consecutive runs / cycles** to be confirmed as a genuine system anomaly. Transient blips and sporadic spikes are filtered out automatically. - **Warning Persistence (4-Run Rule)**: `WARNING` level issues must persist across **at least 4 consecutive runs / cycles** within the 12-hour window to be confirmed as genuine anomalies, automatically filtering out transient blips.
- **Immediate Error Pass**: Critical errors (`ERROR`, `CRITICAL`, `FATAL`) bypass the 4-run threshold and are promoted immediately to `VERIFIED` on their first occurrence.
### 3. Agentic Integration with Hermes ### 3. Agentic Integration with Hermes
Instead of human engineers manually diving through noisy logs, **Hermes** ingests pre-filtered, 4-run validated anomalies directly from the cloud hub (`GET /api/hermes/report`), treating them as verified system artifacts to trigger precise team notifications. Instead of human engineers manually diving through noisy logs, **Hermes** ingests pre-filtered anomalies directly from the cloud hub (`GET /api/hermes/report`), treating verified errors and 4-run validated warnings as actionable system artifacts to trigger precise team notifications.
--- ---
@@ -106,16 +107,20 @@ graph TB
--- ---
## Cloud-Side Temporal Persistence & 4-Run Rule ## Cloud-Side Temporal Persistence & 4-Run Rule
Incoming candidate logs are tracked in SQLite table `active_issues`: Incoming candidate logs are tracked in SQLite table `active_issues`:
- **Issue Fingerprint**: Formatted as `{site_name}:{server}:{signature}`. - **Issue Fingerprint**: Formatted as `{site_name}:{server}:{signature}`.
- **12-Hour Evaluation Window**: - **12-Hour Evaluation Window**:
- When an issue is observed, the hub compares `(now - last_seen)`. - When an issue is observed, the hub compares `(now - last_seen)`.
- If more than 12 hours have passed since the issue was last recorded, the previous window is expired and the cycle resets to `run_count = 1` with status `TRANSIENT`. - If more than 12 hours have passed since the issue was last recorded, the previous window is expired and the cycle resets to `run_count = 1`.
- **4-Run Rule**: - **4-Run Rule for Warnings**:
- The 4-run persistence threshold specifically applies to `WARNING` (and `INFO`) events to eliminate transient operational noise.
- For each distinct run batch, `run_count` increments. - For each distinct run batch, `run_count` increments.
- Issues with `run_count < 4` are marked as `TRANSIENT` and ignored by downstream reporting. - Warnings with `run_count < 4` are marked as `TRANSIENT` and excluded from Hermes reports.
- When `run_count >= 4` within the active 12-hour window, the status transitions to `VERIFIED`. - When `run_count >= 4` within the active 12-hour window, the warning transitions to `VERIFIED`.
- **Immediate Verification for Errors**:
- High-severity events (`ERROR`, `CRITICAL`, `FATAL`) **always pass immediately**.
- On their very first ingestion (`run_count = 1`), errors are promoted directly to `VERIFIED` and surfaced to Hermes without waiting for 4 runs.
--- ---
@@ -124,7 +129,7 @@ Incoming candidate logs are tracked in SQLite table `active_issues`:
The server hub serves a REST reporting API (default port `8443`): The server hub serves a REST reporting API (default port `8443`):
### `GET /api/hermes/report` ### `GET /api/hermes/report`
Returns exclusively **verified anomalies** that have satisfied the 4-run rule within the active 12-hour evaluation window: Returns all **verified anomalies** (immediate critical errors and warnings verified after 4 consecutive runs within the 12-hour evaluation window):
```json ```json
[ [
+1
View File
@@ -6,5 +6,6 @@
- **Removed Client Filter Logic**: Removed restrictive source-level noise filtering on edge forwarders. Clients now collect and stream all candidate events from `INFO` up to `ERROR` over the lookback window instead of discarding them at the source. - **Removed Client Filter Logic**: Removed restrictive source-level noise filtering on edge forwarders. Clients now collect and stream all candidate events from `INFO` up to `ERROR` over the lookback window instead of discarding them at the source.
- **State Tracking & Deduplication**: Added persistent client state tracking (`client_state.json`) with journalctl cursors and Windows Event Log record numbers to guarantee that previously transmitted events are never resent. - **State Tracking & Deduplication**: Added persistent client state tracking (`client_state.json`) with journalctl cursors and Windows Event Log record numbers to guarantee that previously transmitted events are never resent.
- **24-Hour Lookback Window**: Forwarders now scan and upload events from the last 24 hours (default `--hours 24`), skipping older entries. - **24-Hour Lookback Window**: Forwarders now scan and upload events from the last 24 hours (default `--hours 24`), skipping older entries.
- **Warning Persistence & Immediate Error Routing**: Restructured temporal verification on the central hub so the 4-run persistence rule across the 12-hour evaluation window strictly governs `WARNING` and `INFO` events to suppress transient blips. High-severity `ERROR`, `CRITICAL`, and `FATAL` events are now promoted to `VERIFIED` immediately on their first occurrence and reported to Hermes without waiting for consecutive runs.
- **Lightweight Distribution Structure**: Cleaned `out/` to strictly contain deployment documentation and sample configurations. - **Lightweight Distribution Structure**: Cleaned `out/` to strictly contain deployment documentation and sample configurations.
- **Automated Gitea CI/CD**: Integrated push testing workflow (`ci.yml`) and multi-platform release asset packaging. - **Automated Gitea CI/CD**: Integrated push testing workflow (`ci.yml`) and multi-platform release asset packaging.
+3 -3
View File
@@ -11,7 +11,7 @@ Standalone compiled executable binary distribution for Linux server environments
### Key Architecture & Capabilities ### Key Architecture & Capabilities
- **Pre-compiled & Dependency-Free**: Ships as a standalone native Linux ELF binary (`Server.bin`). No Python runtime, pip dependencies, or GnuPG binaries are required on the host system. - **Pre-compiled & Dependency-Free**: Ships as a standalone native Linux ELF binary (`Server.bin`). No Python runtime, pip dependencies, or GnuPG binaries are required on the host system.
- **Authenticated TCP Ingestion Socket (Port 9443)**: Accepts framed OpenPGP encrypted log batches streamed by edge forwarders (`Linux_Client.bin` and `Win_Client.exe`). - **Authenticated TCP Ingestion Socket (Port 9443)**: Accepts framed OpenPGP encrypted log batches streamed by edge forwarders (`Linux_Client.bin` and `Win_Client.exe`).
- **4-Run Temporal Persistence Rule**: Ingested candidate error signatures are evaluated against an episodic threshold. An anomaly must occur across at least 4 distinct client transmission cycles within a sliding 12-hour evaluation window before promotion from transient noise to a `VERIFIED` anomaly. - **Warning Persistence & Immediate Error Routing**: High-severity `ERROR`, `CRITICAL`, and `FATAL` events are promoted to `VERIFIED` immediately on their first occurrence. Operational `WARNING` and `INFO` events are evaluated against an episodic threshold, requiring persistence across at least 4 distinct client transmission cycles within a sliding 12-hour evaluation window before promotion from transient noise to `VERIFIED`.
- **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report` for external scrapers, SIEM collectors, and alerting dashboards. - **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report` for external scrapers, SIEM collectors, and alerting dashboards.
- **Pure-Python OpenPGP Cryptography**: Zero dependency on external `gpg` binaries. Automatically generates RSA-2048 encryption keys and SHA-256 fingerprints on first launch. - **Pure-Python OpenPGP Cryptography**: Zero dependency on external `gpg` binaries. Automatically generates RSA-2048 encryption keys and SHA-256 fingerprints on first launch.
- **State Database**: Tracks anomaly lifecycles, run counters, and machine telemetry in a local SQLite state database (`logar_state.db`). - **State Database**: Tracks anomaly lifecycles, run counters, and machine telemetry in a local SQLite state database (`logar_state.db`).
@@ -68,8 +68,8 @@ The generated `server_config.json` contains:
| `hermes_port` | `8443` | HTTP port for the Hermes reporting endpoint | | `hermes_port` | `8443` | HTTP port for the Hermes reporting endpoint |
| `auth_token` | *(auto-generated)* | Pre-shared secret required in edge client envelopes | | `auth_token` | *(auto-generated)* | Pre-shared secret required in edge client envelopes |
| `db_path` | `"logar_state.db"` | Path to persistent SQLite issue database | | `db_path` | `"logar_state.db"` | Path to persistent SQLite issue database |
| `evaluation_window_hours` | `12` | Sliding temporal window for 4-run rule persistence | | `evaluation_window_hours` | `12` | Sliding temporal window for warning persistence |
| `min_persistence_runs` | `4` | Number of distinct runs required to promote to `VERIFIED` | | `min_persistence_runs` | `4` | Number of distinct runs required to promote warnings to `VERIFIED` |
--- ---
+3 -3
View File
@@ -11,7 +11,7 @@ Standalone compiled executable distribution for Windows Server environments (`Se
### Key Architecture & Capabilities ### Key Architecture & Capabilities
- **Pre-compiled & Dependency-Free**: Ships as a standalone Windows executable (`Server.exe`). No Python installation, pip packages, or GnuPG binaries are required on Windows Server. - **Pre-compiled & Dependency-Free**: Ships as a standalone Windows executable (`Server.exe`). No Python installation, pip packages, or GnuPG binaries are required on Windows Server.
- **Authenticated TCP Ingestion Socket (Port 9443)**: Ingests framed OpenPGP encrypted log batches streamed from edge forwarder nodes (`Win_Client.exe` and `Linux_Client.bin`). - **Authenticated TCP Ingestion Socket (Port 9443)**: Ingests framed OpenPGP encrypted log batches streamed from edge forwarder nodes (`Win_Client.exe` and `Linux_Client.bin`).
- **4-Run Temporal Persistence Rule**: Filters transient noise by requiring an issue signature to recur across at least 4 episodic transmission cycles within a rolling 12-hour evaluation window before promotion to `VERIFIED`. - **Warning Persistence & Immediate Error Routing**: High-severity `ERROR`, `CRITICAL`, and `FATAL` events are promoted to `VERIFIED` immediately on their first occurrence. Operational `WARNING` and `INFO` events are evaluated against an episodic threshold, requiring persistence across at least 4 distinct client transmission cycles within a rolling 12-hour evaluation window before promotion to `VERIFIED`.
- **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report` for external dashboards, monitoring agents, and scrapers. - **Embedded Hermes Reporting API (Port 8443)**: Integrated REST API exposing `/api/hermes/report` for external dashboards, monitoring agents, and scrapers.
- **Pure-Python OpenPGP Cryptography**: Automatically generates RSA-2048 encryption keys and a SHA-256 fingerprint on first launch without external dependencies. - **Pure-Python OpenPGP Cryptography**: Automatically generates RSA-2048 encryption keys and a SHA-256 fingerprint on first launch without external dependencies.
- **State Database**: Stores issue lifecycle records, run counters, and machine telemetry in a local SQLite database (`logar_state.db`). - **State Database**: Stores issue lifecycle records, run counters, and machine telemetry in a local SQLite database (`logar_state.db`).
@@ -68,8 +68,8 @@ The generated `server_config.json` contains:
| `hermes_port` | `8443` | HTTP port for the Hermes reporting endpoint | | `hermes_port` | `8443` | HTTP port for the Hermes reporting endpoint |
| `auth_token` | *(auto-generated)* | Pre-shared authentication secret required in client envelopes | | `auth_token` | *(auto-generated)* | Pre-shared authentication secret required in client envelopes |
| `db_path` | `"logar_state.db"` | Path to persistent SQLite issue database | | `db_path` | `"logar_state.db"` | Path to persistent SQLite issue database |
| `evaluation_window_hours` | `12` | Rolling evaluation window in hours for 4-run rule | | `evaluation_window_hours` | `12` | Rolling evaluation window in hours for warning persistence |
| `min_persistence_runs` | `4` | Consecutive runs required to promote an issue to `VERIFIED` | | `min_persistence_runs` | `4` | Consecutive runs required to promote warning issues to `VERIFIED` |
--- ---
+12 -7
View File
@@ -183,6 +183,9 @@ def process_ingested_logs(payload: Dict[str, Any], db_path: str, window_hours: i
if severity in ["DEBUG", "TRACE"]: if severity in ["DEBUG", "TRACE"]:
continue continue
# Errors are always passed immediately; the 4-run rule only concerns warnings
is_error = severity in ["ERROR", "CRITICAL", "FATAL"]
signature = log.get("signature", "unknown") signature = log.get("signature", "unknown")
server = log.get("server", client_server) server = log.get("server", client_server)
message = log.get("message", "") message = log.get("message", "")
@@ -207,7 +210,7 @@ def process_ingested_logs(payload: Dict[str, Any], db_path: str, window_hours: i
# Window elapsed: reset to new cycle # Window elapsed: reset to new cycle
new_runs = 1 new_runs = 1
new_first_seen = now_iso new_first_seen = now_iso
new_status = "TRANSIENT" new_status = "VERIFIED" if is_error else "TRANSIENT"
else: else:
# Same run guard: only increment count once per distinct run batch # Same run guard: only increment count once per distinct run batch
if last_run_id != run_id: if last_run_id != run_id:
@@ -215,8 +218,8 @@ def process_ingested_logs(payload: Dict[str, Any], db_path: str, window_hours: i
else: else:
new_runs = run_count new_runs = run_count
new_first_seen = first_seen_str new_first_seen = first_seen_str
# 4-run rule enforcement # 4-run rule applies to warnings; errors are always passed immediately as VERIFIED
new_status = "VERIFIED" if new_runs >= min_runs else "TRANSIENT" new_status = "VERIFIED" if (is_error or new_runs >= min_runs) else "TRANSIENT"
if new_status == "VERIFIED" and current_status != "VERIFIED": if new_status == "VERIFIED" and current_status != "VERIFIED":
promoted_to_verified += 1 promoted_to_verified += 1
@@ -227,7 +230,9 @@ def process_ingested_logs(payload: Dict[str, Any], db_path: str, window_hours: i
WHERE fingerprint = ? WHERE fingerprint = ?
""", (new_runs, now_iso, new_first_seen, new_status, run_id, message, severity, fp)) """, (new_runs, now_iso, new_first_seen, new_status, run_id, message, severity, fp))
else: else:
initial_status = "VERIFIED" if 1 >= min_runs else "TRANSIENT" initial_status = "VERIFIED" if (is_error or 1 >= min_runs) else "TRANSIENT"
if initial_status == "VERIFIED":
promoted_to_verified += 1
cursor.execute(""" cursor.execute("""
INSERT INTO active_issues INSERT INTO active_issues
(fingerprint, site_name, server, signature, severity, message, os_type, first_seen, last_seen, run_count, status, last_run_id) (fingerprint, site_name, server, signature, severity, message, os_type, first_seen, last_seen, run_count, status, last_run_id)
@@ -328,8 +333,8 @@ def get_hermes_report():
cursor.execute(""" cursor.execute("""
SELECT fingerprint, site_name, server, signature, severity, message, os_type, first_seen, last_seen, run_count, status SELECT fingerprint, site_name, server, signature, severity, message, os_type, first_seen, last_seen, run_count, status
FROM active_issues FROM active_issues
WHERE status = 'VERIFIED' AND run_count >= ? WHERE status = 'VERIFIED'
""", (min_runs,)) """)
rows = cursor.fetchall() rows = cursor.fetchall()
conn.close() conn.close()
@@ -452,7 +457,7 @@ def main():
print("=" * 60) print("=" * 60)
print(f" LOGAR Server Hub: {config['server_name']}") print(f" LOGAR Server Hub: {config['server_name']}")
print(f" Encryption Fingerprint: {config['server_fingerprint']}") print(f" Encryption Fingerprint: {config['server_fingerprint']}")
print(f" Evaluation Window: {config['evaluation_window_hours']} hours | Rule: {config['min_persistence_runs']}+ consecutive runs") print(f" Evaluation Window: {config['evaluation_window_hours']} hours | 4-Run Rule: Warnings | Immediate Pass: Errors")
print("=" * 60) print("=" * 60)
try: try:
+46 -9
View File
@@ -80,20 +80,25 @@ def run_tests():
assert bad_resp.get("status") == "error", f"Expected error, got: {bad_resp}" assert bad_resp.get("status") == "error", f"Expected error, got: {bad_resp}"
print(f"[OK] Bad auth rejected correctly: {bad_resp['message']}") print(f"[OK] Bad auth rejected correctly: {bad_resp['message']}")
test_signature = "TestServiceCrash" test_signature = "TestServiceDegraded"
candidate_log = [{ candidate_log = [{
"server": "test-edge-node", "server": "test-edge-node",
"os_type": "linux", "os_type": "linux",
"signature": test_signature, "signature": test_signature,
"severity": "ERROR", "severity": "WARNING",
"message": "Out of memory killer triggered" "message": "Resource usage high warning"
}] }]
print("\n=== [3] Testing Temporal Persistence & 4-Run Rule ===") print("\n=== [3] Testing Temporal Persistence & 4-Run Rule for Warnings ===")
for run_num in range(1, 5): for run_num in range(1, 5):
resp = send_socket_batch(candidate_log) resp = send_socket_batch(candidate_log)
assert resp.get("status") == "success", f"Run {run_num} failed: {resp}" assert resp.get("status") == "success", f"Run {run_num} failed: {resp}"
print(f"[Run {run_num}/4] Ingested successfully. Promoted to verified: {resp.get('promoted_verified')}") promoted = resp.get("promoted_verified", 0)
print(f"[Run {run_num}/4] Ingested successfully. Promoted to verified: {promoted}")
if run_num < 4:
assert promoted == 0, f"Expected 0 promoted on run {run_num} for warning, got {promoted}"
else:
assert promoted == 1, f"Expected 1 promoted on run 4 for warning, got {promoted}"
# Inspect SQLite database directly # Inspect SQLite database directly
conn = sqlite3.connect(server_conf.get("db_path", "logar_state.db")) conn = sqlite3.connect(server_conf.get("db_path", "logar_state.db"))
@@ -107,7 +112,33 @@ def run_tests():
print(f"[DB Verification] Issue '{test_signature}' -> run_count: {run_count}, status: {status}") print(f"[DB Verification] Issue '{test_signature}' -> run_count: {run_count}, status: {status}")
assert run_count >= 4, f"Expected run_count >= 4, got {run_count}" assert run_count >= 4, f"Expected run_count >= 4, got {run_count}"
assert status == "VERIFIED", f"Expected status 'VERIFIED', got {status}" assert status == "VERIFIED", f"Expected status 'VERIFIED', got {status}"
print("[OK] 4-Run Rule verified: Transient issue promoted to VERIFIED anomaly!") print("[OK] 4-Run Rule verified: Warning promoted to VERIFIED anomaly on 4th run!")
print("\n=== [3b] Testing Immediate Pass for Errors ===")
error_signature = "TestServiceCrashImmediate"
error_log = [{
"server": "test-edge-node",
"os_type": "linux",
"signature": error_signature,
"severity": "ERROR",
"message": "Fatal process crash occurred"
}]
err_resp = send_socket_batch(error_log)
assert err_resp.get("status") == "success", f"Error run failed: {err_resp}"
print(f"[Run 1/1] Error ingested successfully. Promoted to verified: {err_resp.get('promoted_verified')}")
assert err_resp.get("promoted_verified") == 1, f"Expected error to be promoted to verified immediately on run 1, got {err_resp.get('promoted_verified')}"
conn = sqlite3.connect(server_conf.get("db_path", "logar_state.db"))
cursor = conn.cursor()
cursor.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", (error_signature,))
err_row = cursor.fetchone()
conn.close()
assert err_row is not None, "Error issue not found in SQLite"
err_run_count, err_status = err_row
print(f"[DB Verification] Issue '{error_signature}' -> run_count: {err_run_count}, status: {err_status}")
assert err_run_count == 1, f"Expected run_count == 1, got {err_run_count}"
assert err_status == "VERIFIED", f"Expected status 'VERIFIED', got {err_status}"
print("[OK] Immediate pass verified: Error promoted to VERIFIED anomaly immediately!")
print("\n=== [4] Testing Hermes Reporting Endpoint (/api/hermes/report) ===") print("\n=== [4] Testing Hermes Reporting Endpoint (/api/hermes/report) ===")
req = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/hermes/report") req = urllib.request.Request(f"http://{HERMES_HOST}:{HERMES_PORT}/api/hermes/report")
@@ -116,15 +147,21 @@ def run_tests():
hermes_data = json.loads(response.read().decode("utf-8")) hermes_data = json.loads(response.read().decode("utf-8"))
print(f"[Hermes API] Returned {len(hermes_data)} verified anomalies:") print(f"[Hermes API] Returned {len(hermes_data)} verified anomalies:")
found_issue = False found_warning = False
found_error = False
for issue in hermes_data: for issue in hermes_data:
print(f" - Fingerprint: {issue['fingerprint']} | Consecutive Runs: {issue['consecutive_runs']} | Status: {issue['status']}") print(f" - Fingerprint: {issue['fingerprint']} | Consecutive Runs: {issue['consecutive_runs']} | Status: {issue['status']}")
if issue["signature"] == test_signature: if issue["signature"] == test_signature:
found_issue = True found_warning = True
assert issue["verified"] is True assert issue["verified"] is True
assert issue["consecutive_runs"] >= 4 assert issue["consecutive_runs"] >= 4
if issue["signature"] == error_signature:
found_error = True
assert issue["verified"] is True
assert issue["consecutive_runs"] == 1
assert found_issue, f"Test issue {test_signature} should be in Hermes report" assert found_warning, f"Warning issue {test_signature} should be in Hermes report"
assert found_error, f"Error issue {error_signature} should be in Hermes report"
print("[OK] Hermes reporting validated!") print("[OK] Hermes reporting validated!")
print("\n=== [5] Testing Windows Client Script Integration ===") print("\n=== [5] Testing Windows Client Script Integration ===")
+48 -14
View File
@@ -79,9 +79,9 @@ class TestServerComponent(unittest.TestCase):
Server.init_db(self.test_db) Server.init_db(self.test_db)
log_entry = { log_entry = {
"server": "app-worker-01.corp.local", "server": "app-worker-01.corp.local",
"signature": "PostgresConnTimeout", "signature": "PostgresConnWarning",
"severity": "ERROR", "severity": "WARNING",
"message": "Connection to database pool timed out after 30s", "message": "Connection to database pool near capacity: 85%",
"os_type": "linux" "os_type": "linux"
} }
payload = { payload = {
@@ -89,7 +89,7 @@ class TestServerComponent(unittest.TestCase):
"logs": [log_entry] "logs": [log_entry]
} }
# Runs 1 to 3: should remain TRANSIENT # Runs 1 to 3: WARNING should remain TRANSIENT
for run_idx in range(1, 4): for run_idx in range(1, 4):
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4) res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4)
self.assertEqual(res["status"], "success") self.assertEqual(res["status"], "success")
@@ -97,24 +97,53 @@ class TestServerComponent(unittest.TestCase):
conn = sqlite3.connect(self.test_db) conn = sqlite3.connect(self.test_db)
c = conn.cursor() c = conn.cursor()
c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnTimeout",)) c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnWarning",))
row = c.fetchone() row = c.fetchone()
conn.close() conn.close()
self.assertEqual(row[0], 3) self.assertEqual(row[0], 3)
self.assertEqual(row[1], "TRANSIENT") self.assertEqual(row[1], "TRANSIENT")
# Run 4: promotes to VERIFIED! # Run 4: promotes WARNING to VERIFIED!
res4 = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4) res4 = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4)
self.assertEqual(res4["promoted_verified"], 1) self.assertEqual(res4["promoted_verified"], 1)
conn = sqlite3.connect(self.test_db) conn = sqlite3.connect(self.test_db)
c = conn.cursor() c = conn.cursor()
c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnTimeout",)) c.execute("SELECT run_count, status FROM active_issues WHERE signature = ?", ("PostgresConnWarning",))
row = c.fetchone() row = c.fetchone()
conn.close() conn.close()
self.assertEqual(row[0], 4) self.assertEqual(row[0], 4)
self.assertEqual(row[1], "VERIFIED") self.assertEqual(row[1], "VERIFIED")
def test_error_immediate_pass(self):
Server.init_db(self.test_db)
log_entry = {
"server": "app-worker-01.corp.local",
"signature": "KernelPanicCritical",
"severity": "ERROR",
"message": "Kernel panic - not syncing: Fatal hardware error",
"os_type": "linux"
}
payload = {
"server": "app-worker-01.corp.local",
"logs": [log_entry]
}
# Run 1: ERROR must immediately promote to VERIFIED
res = Server.process_ingested_logs(payload, self.test_db, window_hours=12, min_runs=4)
self.assertEqual(res["status"], "success")
self.assertEqual(res["promoted_verified"], 1)
conn = sqlite3.connect(self.test_db)
c = conn.cursor()
c.execute("SELECT run_count, status, severity FROM active_issues WHERE signature = ?", ("KernelPanicCritical",))
row = c.fetchone()
conn.close()
self.assertIsNotNone(row)
self.assertEqual(row[0], 1)
self.assertEqual(row[1], "VERIFIED")
self.assertEqual(row[2], "ERROR")
def test_server_severity_filtering(self): def test_server_severity_filtering(self):
Server.init_db(self.test_db) Server.init_db(self.test_db)
payload = { payload = {
@@ -132,15 +161,20 @@ class TestServerComponent(unittest.TestCase):
conn = sqlite3.connect(self.test_db) conn = sqlite3.connect(self.test_db)
c = conn.cursor() c = conn.cursor()
c.execute("SELECT signature FROM active_issues ORDER BY signature") c.execute("SELECT signature, status FROM active_issues ORDER BY signature")
sigs = [r[0] for r in c.fetchall()] rows = dict(c.fetchall())
conn.close() conn.close()
self.assertIn("SigInfo", sigs) self.assertIn("SigInfo", rows)
self.assertIn("SigWarn", sigs) self.assertIn("SigWarn", rows)
self.assertIn("SigErr", sigs) self.assertIn("SigErr", rows)
self.assertNotIn("SigDebug", sigs) self.assertNotIn("SigDebug", rows)
self.assertNotIn("SigTrace", sigs) self.assertNotIn("SigTrace", rows)
# SigErr is immediately VERIFIED; SigWarn and SigInfo are TRANSIENT on run 1
self.assertEqual(rows["SigErr"], "VERIFIED")
self.assertEqual(rows["SigWarn"], "TRANSIENT")
self.assertEqual(rows["SigInfo"], "TRANSIENT")
if __name__ == "__main__": if __name__ == "__main__":