@simbimbo/brainstem 0.0.3 → 0.0.5
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.
- package/CHANGELOG.md +26 -0
- package/README.md +26 -0
- package/brainstem/__init__.py +1 -1
- package/brainstem/adapters.py +120 -0
- package/brainstem/api.py +468 -57
- package/brainstem/config.py +136 -0
- package/brainstem/connectors/logicmonitor.py +57 -0
- package/brainstem/demo.py +16 -2
- package/brainstem/fingerprint.py +54 -0
- package/brainstem/ingest.py +440 -33
- package/brainstem/interesting.py +56 -1
- package/brainstem/listener.py +181 -0
- package/brainstem/models.py +1 -0
- package/brainstem/recurrence.py +63 -9
- package/brainstem/scoring.py +6 -4
- package/brainstem/source_drivers.py +179 -0
- package/brainstem/storage.py +389 -12
- package/docs/README.md +103 -0
- package/docs/api.md +260 -280
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/tests/test_adapters.py +95 -0
- package/tests/test_api.py +812 -0
- package/tests/test_canonicalization.py +8 -0
- package/tests/test_config.py +39 -0
- package/tests/test_file_ingest.py +77 -0
- package/tests/test_fingerprint.py +51 -1
- package/tests/test_interesting.py +10 -0
- package/tests/test_listener.py +253 -0
- package/tests/test_logicmonitor.py +54 -1
- package/tests/test_recurrence.py +16 -0
- package/tests/test_source_drivers.py +95 -0
- package/tests/test_storage.py +178 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.0.5 — 2026-03-22
|
|
4
|
+
|
|
5
|
+
Source capability reporting and docs/runtime alignment pass.
|
|
6
|
+
|
|
7
|
+
### Highlights
|
|
8
|
+
- exposes compact source-support matrix in `/runtime` (`capability_flags.source_capabilities`)
|
|
9
|
+
- documents LogicMonitor-shaped ingest path as implemented runtime API surface
|
|
10
|
+
- aligns README/docs/examples with runtime API surface and active source drivers
|
|
11
|
+
- keeps behavior intact for existing ingestion/canonicalization paths
|
|
12
|
+
|
|
13
|
+
### Validation
|
|
14
|
+
- local test suite passed (with updated runtime capability coverage)
|
|
15
|
+
|
|
16
|
+
## 0.0.4 — 2026-03-22
|
|
17
|
+
|
|
18
|
+
Release-prep alignment for the current intake foundation state.
|
|
19
|
+
|
|
20
|
+
### Highlights
|
|
21
|
+
- aligns packaging/runtime version metadata for the next release cut (`0.0.4` in `package.json`, `pyproject.toml`, and `brainstem.__version__`)
|
|
22
|
+
- updates README/API-facing documentation to call out only implemented runtime and listener surfaces
|
|
23
|
+
- keeps claims bounded to implemented behavior; no new feature claims
|
|
24
|
+
|
|
25
|
+
### Notes
|
|
26
|
+
- no runtime behavior or interface changes in this release-prep pass
|
|
27
|
+
- no publish action performed
|
|
28
|
+
|
|
3
29
|
## 0.0.3 — 2026-03-22
|
|
4
30
|
|
|
5
31
|
Intake Foundation follow-up release for **brAInstem**.
|
package/README.md
CHANGED
|
@@ -48,6 +48,32 @@ For a given tenant or environment, brAInstem should answer:
|
|
|
48
48
|
- Have we seen this before?
|
|
49
49
|
- What happened right before the last similar incident?
|
|
50
50
|
|
|
51
|
+
## Current implemented runtime surface
|
|
52
|
+
|
|
53
|
+
Current implementation is intentionally bounded:
|
|
54
|
+
- FastAPI runtime at `brainstem.api:app`
|
|
55
|
+
- Implemented API endpoints include:
|
|
56
|
+
- `POST /ingest/event`, `POST /ingest/batch`, `POST /ingest/logicmonitor`, `POST /replay/raw`
|
|
57
|
+
- `GET /interesting`, `GET /candidates`, `GET /signatures`, `GET /canonical_events`
|
|
58
|
+
- `GET /stats`, `GET /failures`, `GET /failures/{id}`
|
|
59
|
+
- `GET /raw_envelopes`, `GET /ingest/recent`, `GET /sources`, `GET /sources/status`
|
|
60
|
+
- `GET /runtime`, `GET /status`, `GET /healthz`
|
|
61
|
+
- UDP syslog listener at `brainstem.listener`
|
|
62
|
+
- source-driver intake for `syslog`, `file`, and narrow `logicmonitor` connector payload shape
|
|
63
|
+
- SQLite-backed persistence with default DB at `.brainstem-state/brainstem.sqlite3`
|
|
64
|
+
- optional API token in `BRAINSTEM_API_TOKEN`
|
|
65
|
+
|
|
66
|
+
Runtime config can also be inspected at `/runtime`. The runtime summary now includes explicit `source_capabilities`
|
|
67
|
+
(`source_types` + per-source ingest modes) and auth/capabilities flags.
|
|
68
|
+
|
|
69
|
+
## Quick run path (API + listener + file/syslog intake)
|
|
70
|
+
|
|
71
|
+
See the compact run surface in:
|
|
72
|
+
|
|
73
|
+
- [`docs/README.md`](docs/README.md)
|
|
74
|
+
|
|
75
|
+
It covers API startup, listener ingest, and file ingest end-to-end with only implemented entry points and current payload shapes.
|
|
76
|
+
|
|
51
77
|
## Relationship to ocmemog
|
|
52
78
|
|
|
53
79
|
Shared DNA:
|
package/brainstem/__init__.py
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Dict, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
from .models import RawInputEnvelope
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@runtime_checkable
|
|
11
|
+
class RawInputAdapter(Protocol):
|
|
12
|
+
"""Contract for source-specific envelope producers."""
|
|
13
|
+
|
|
14
|
+
source_type: str
|
|
15
|
+
|
|
16
|
+
def parse_raw_input(
|
|
17
|
+
self,
|
|
18
|
+
payload: Any,
|
|
19
|
+
*,
|
|
20
|
+
tenant_id: str,
|
|
21
|
+
source_path: str = "",
|
|
22
|
+
) -> RawInputEnvelope:
|
|
23
|
+
"""Build a RawInputEnvelope from source payload."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_ADAPTER_REGISTRY: Dict[str, RawInputAdapter] = {}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def register_raw_input_adapter(adapter: RawInputAdapter) -> None:
|
|
30
|
+
"""Register/replace an adapter for a source type."""
|
|
31
|
+
|
|
32
|
+
_ADAPTER_REGISTRY[adapter.source_type] = adapter
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_raw_input_adapter(source_type: str) -> RawInputAdapter:
|
|
36
|
+
"""Return the adapter for `source_type`."""
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
return _ADAPTER_REGISTRY[source_type]
|
|
40
|
+
except KeyError as exc:
|
|
41
|
+
raise ValueError(f"unsupported source_type: {source_type}") from exc
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def list_raw_input_source_types() -> list[str]:
|
|
45
|
+
"""Return currently registered source types (inspectable list)."""
|
|
46
|
+
|
|
47
|
+
return sorted(_ADAPTER_REGISTRY.keys())
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def build_raw_input_envelope(
|
|
51
|
+
source_type: str,
|
|
52
|
+
payload: Any,
|
|
53
|
+
*,
|
|
54
|
+
tenant_id: str,
|
|
55
|
+
source_path: str = "",
|
|
56
|
+
) -> RawInputEnvelope:
|
|
57
|
+
"""Build a RawInputEnvelope using the source-type registry."""
|
|
58
|
+
|
|
59
|
+
adapter = get_raw_input_adapter(source_type)
|
|
60
|
+
return adapter.parse_raw_input(payload, tenant_id=tenant_id, source_path=source_path)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class SyslogRawInputAdapter:
|
|
65
|
+
"""Simple adapter for syslog-like line-oriented payloads."""
|
|
66
|
+
|
|
67
|
+
source_type: str = "syslog"
|
|
68
|
+
|
|
69
|
+
def parse_raw_input(self, payload: Any, *, tenant_id: str, source_path: str = "") -> RawInputEnvelope:
|
|
70
|
+
text = ("" if payload is None else str(payload)).rstrip("\n")
|
|
71
|
+
timestamp = datetime.utcnow().isoformat() + "Z"
|
|
72
|
+
host = ""
|
|
73
|
+
service = ""
|
|
74
|
+
message = text
|
|
75
|
+
|
|
76
|
+
parts = text.split()
|
|
77
|
+
if len(parts) >= 5:
|
|
78
|
+
host = parts[3]
|
|
79
|
+
rest = " ".join(parts[4:])
|
|
80
|
+
if ":" in rest:
|
|
81
|
+
svc, _, msg = rest.partition(":")
|
|
82
|
+
service = svc.strip()
|
|
83
|
+
message = msg.strip() or rest.strip()
|
|
84
|
+
else:
|
|
85
|
+
message = rest.strip()
|
|
86
|
+
|
|
87
|
+
return RawInputEnvelope(
|
|
88
|
+
tenant_id=tenant_id,
|
|
89
|
+
source_type=self.source_type,
|
|
90
|
+
timestamp=timestamp,
|
|
91
|
+
message_raw=message,
|
|
92
|
+
host=host,
|
|
93
|
+
service=service,
|
|
94
|
+
source_path=source_path,
|
|
95
|
+
metadata={"raw_line": text},
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass(frozen=True)
|
|
100
|
+
class FileRawInputAdapter:
|
|
101
|
+
"""Simple adapter for generic line-oriented log files."""
|
|
102
|
+
|
|
103
|
+
source_type: str = "file"
|
|
104
|
+
|
|
105
|
+
def parse_raw_input(self, payload: Any, *, tenant_id: str, source_path: str = "") -> RawInputEnvelope:
|
|
106
|
+
text = "" if payload is None else str(payload)
|
|
107
|
+
timestamp = datetime.utcnow().isoformat() + "Z"
|
|
108
|
+
|
|
109
|
+
return RawInputEnvelope(
|
|
110
|
+
tenant_id=tenant_id,
|
|
111
|
+
source_type=self.source_type,
|
|
112
|
+
timestamp=timestamp,
|
|
113
|
+
message_raw=text.rstrip("\n"),
|
|
114
|
+
source_path=source_path,
|
|
115
|
+
metadata={"raw_line": text},
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
register_raw_input_adapter(SyslogRawInputAdapter())
|
|
120
|
+
register_raw_input_adapter(FileRawInputAdapter())
|