@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
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import socket
|
|
6
|
+
from dataclasses import asdict
|
|
7
|
+
from typing import Any, Callable, List, Optional
|
|
8
|
+
|
|
9
|
+
from .ingest import IngestionResult, run_ingest_source_payload
|
|
10
|
+
from .ingest import canonicalize_raw_input_envelope
|
|
11
|
+
from .config import get_runtime_config
|
|
12
|
+
from .source_drivers import parse_source_payloads
|
|
13
|
+
from .models import CanonicalEvent
|
|
14
|
+
from .models import RawInputEnvelope
|
|
15
|
+
|
|
16
|
+
EventHandler = Callable[[CanonicalEvent], None]
|
|
17
|
+
ErrorHandler = Callable[[Exception, str], None]
|
|
18
|
+
LISTENER_CONFIG = get_runtime_config().listener
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_syslog_datagram(
|
|
22
|
+
payload: bytes,
|
|
23
|
+
*,
|
|
24
|
+
tenant_id: str,
|
|
25
|
+
source_path: str = LISTENER_CONFIG.syslog_source_path,
|
|
26
|
+
on_parse_error: Optional[ErrorHandler] = None,
|
|
27
|
+
) -> List[CanonicalEvent]:
|
|
28
|
+
raw_events = parse_source_payloads(
|
|
29
|
+
"syslog",
|
|
30
|
+
payload,
|
|
31
|
+
tenant_id=tenant_id,
|
|
32
|
+
source_path=source_path,
|
|
33
|
+
on_parse_error=on_parse_error,
|
|
34
|
+
)
|
|
35
|
+
events: List[CanonicalEvent] = []
|
|
36
|
+
for raw_event in raw_events:
|
|
37
|
+
try:
|
|
38
|
+
events.append(canonicalize_raw_input_envelope(raw_event))
|
|
39
|
+
except Exception as exc:
|
|
40
|
+
if on_parse_error is None:
|
|
41
|
+
raise
|
|
42
|
+
on_parse_error(exc, raw_event.metadata.get("raw_line", raw_event.message_raw))
|
|
43
|
+
return events
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_syslog_raw_datagram(
|
|
47
|
+
payload: bytes,
|
|
48
|
+
*,
|
|
49
|
+
tenant_id: str,
|
|
50
|
+
source_path: str = LISTENER_CONFIG.syslog_source_path,
|
|
51
|
+
on_parse_error: Optional[ErrorHandler] = None,
|
|
52
|
+
) -> List[RawInputEnvelope]:
|
|
53
|
+
return parse_source_payloads(
|
|
54
|
+
"syslog",
|
|
55
|
+
payload,
|
|
56
|
+
tenant_id=tenant_id,
|
|
57
|
+
source_path=source_path,
|
|
58
|
+
on_parse_error=on_parse_error,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def run_ingest_syslog_datagram(
|
|
63
|
+
payload: bytes,
|
|
64
|
+
*,
|
|
65
|
+
tenant_id: str,
|
|
66
|
+
source_path: str = LISTENER_CONFIG.syslog_source_path,
|
|
67
|
+
threshold: int | None = None,
|
|
68
|
+
db_path: Optional[str] = None,
|
|
69
|
+
on_event: Optional[EventHandler] = None,
|
|
70
|
+
on_parse_error: Optional[ErrorHandler] = None,
|
|
71
|
+
) -> IngestionResult:
|
|
72
|
+
if threshold is None:
|
|
73
|
+
threshold = get_runtime_config().listener.ingest_threshold
|
|
74
|
+
|
|
75
|
+
return run_ingest_source_payload(
|
|
76
|
+
"syslog",
|
|
77
|
+
payload,
|
|
78
|
+
tenant_id=tenant_id,
|
|
79
|
+
source_path=source_path,
|
|
80
|
+
threshold=threshold,
|
|
81
|
+
db_path=db_path,
|
|
82
|
+
on_event=on_event,
|
|
83
|
+
on_parse_error=on_parse_error,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_udp_syslog_listener(
|
|
88
|
+
tenant_id: str,
|
|
89
|
+
*,
|
|
90
|
+
host: str = LISTENER_CONFIG.syslog_host,
|
|
91
|
+
port: int = LISTENER_CONFIG.syslog_port,
|
|
92
|
+
source_path: Optional[str] = None,
|
|
93
|
+
on_event: Optional[EventHandler] = None,
|
|
94
|
+
on_parse_error: Optional[ErrorHandler] = None,
|
|
95
|
+
db_path: Optional[str] = None,
|
|
96
|
+
threshold: int | None = None,
|
|
97
|
+
max_datagrams: Optional[int] = None,
|
|
98
|
+
socket_timeout: float = LISTENER_CONFIG.syslog_socket_timeout,
|
|
99
|
+
socket_obj: Optional[socket.socket] = None,
|
|
100
|
+
) -> int:
|
|
101
|
+
if threshold is None:
|
|
102
|
+
threshold = get_runtime_config().listener.ingest_threshold
|
|
103
|
+
|
|
104
|
+
sock = socket_obj
|
|
105
|
+
owns_socket = sock is None
|
|
106
|
+
if sock is None:
|
|
107
|
+
sock = socket.socket(family=socket.AF_INET, type=socket.SOCK_DGRAM)
|
|
108
|
+
try:
|
|
109
|
+
bound_host, bound_port = sock.getsockname()
|
|
110
|
+
if bound_port == 0:
|
|
111
|
+
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
112
|
+
sock.bind((host, port))
|
|
113
|
+
bound_host, bound_port = sock.getsockname()
|
|
114
|
+
if source_path is None:
|
|
115
|
+
source_path = f"udp://{bound_host}:{bound_port}"
|
|
116
|
+
|
|
117
|
+
received_events = 0
|
|
118
|
+
sock.settimeout(socket_timeout)
|
|
119
|
+
|
|
120
|
+
processed_datagrams = 0
|
|
121
|
+
while True:
|
|
122
|
+
if max_datagrams is not None and processed_datagrams >= max_datagrams:
|
|
123
|
+
break
|
|
124
|
+
try:
|
|
125
|
+
payload, _ = sock.recvfrom(65535)
|
|
126
|
+
except socket.timeout:
|
|
127
|
+
continue
|
|
128
|
+
processed_datagrams += 1
|
|
129
|
+
if db_path is None:
|
|
130
|
+
events = parse_syslog_datagram(
|
|
131
|
+
payload,
|
|
132
|
+
tenant_id=tenant_id,
|
|
133
|
+
source_path=source_path,
|
|
134
|
+
on_parse_error=on_parse_error,
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
result = run_ingest_syslog_datagram(
|
|
138
|
+
payload,
|
|
139
|
+
tenant_id=tenant_id,
|
|
140
|
+
source_path=source_path,
|
|
141
|
+
threshold=threshold,
|
|
142
|
+
db_path=db_path,
|
|
143
|
+
on_event=None,
|
|
144
|
+
on_parse_error=on_parse_error,
|
|
145
|
+
)
|
|
146
|
+
events = result.events
|
|
147
|
+
for event in events:
|
|
148
|
+
received_events += 1
|
|
149
|
+
if on_event is not None:
|
|
150
|
+
on_event(event)
|
|
151
|
+
return received_events
|
|
152
|
+
finally:
|
|
153
|
+
if owns_socket:
|
|
154
|
+
sock.close()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def main(argv: Optional[list[str]] = None) -> int:
|
|
158
|
+
parser = argparse.ArgumentParser(description="Run the brAInstem UDP syslog listener.")
|
|
159
|
+
parser.add_argument("--host", default=LISTENER_CONFIG.syslog_host, help="Listen host")
|
|
160
|
+
parser.add_argument("--port", type=int, default=LISTENER_CONFIG.syslog_port, help="UDP port")
|
|
161
|
+
parser.add_argument("--tenant", default="demo-tenant", help="Tenant identifier")
|
|
162
|
+
parser.add_argument("--source-path", default="", help="Override source_path metadata on parsed envelopes")
|
|
163
|
+
args = parser.parse_args(argv)
|
|
164
|
+
|
|
165
|
+
source_path = args.source_path or f"udp://{args.host}:{args.port}"
|
|
166
|
+
|
|
167
|
+
def _emit(event: CanonicalEvent) -> None:
|
|
168
|
+
print(json.dumps(asdict(event), default=str))
|
|
169
|
+
|
|
170
|
+
run_udp_syslog_listener(
|
|
171
|
+
args.tenant,
|
|
172
|
+
host=args.host,
|
|
173
|
+
port=args.port,
|
|
174
|
+
source_path=source_path,
|
|
175
|
+
on_event=_emit,
|
|
176
|
+
)
|
|
177
|
+
return 0
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
raise SystemExit(main())
|
package/brainstem/models.py
CHANGED
package/brainstem/recurrence.py
CHANGED
|
@@ -5,13 +5,17 @@ from dataclasses import asdict
|
|
|
5
5
|
from typing import Iterable, List
|
|
6
6
|
|
|
7
7
|
from .models import Candidate, Event, Signature
|
|
8
|
+
from .interesting import _attention_explanation
|
|
8
9
|
from .scoring import score_candidate
|
|
10
|
+
from .config import get_runtime_config
|
|
9
11
|
|
|
10
12
|
|
|
11
13
|
FAMILY_TITLES = {
|
|
12
14
|
"failure": "Recurring failure pattern",
|
|
13
15
|
"auth": "Recurring authentication failure pattern",
|
|
14
16
|
"service_lifecycle": "Recurring service lifecycle instability",
|
|
17
|
+
"connectivity": "Recurring network connectivity pattern",
|
|
18
|
+
"resource": "Recurring resource pressure pattern",
|
|
15
19
|
"generic": "Recurring operational pattern",
|
|
16
20
|
}
|
|
17
21
|
|
|
@@ -44,20 +48,65 @@ def signature_counts(signatures: Iterable[Signature]) -> Counter:
|
|
|
44
48
|
return Counter(sig.signature_key for sig in signatures)
|
|
45
49
|
|
|
46
50
|
|
|
47
|
-
def
|
|
51
|
+
def _coerce_raw_envelope_id(value: object) -> int | None:
|
|
52
|
+
if isinstance(value, bool):
|
|
53
|
+
return None
|
|
54
|
+
if isinstance(value, int):
|
|
55
|
+
return value
|
|
56
|
+
if isinstance(value, str):
|
|
57
|
+
value = value.strip()
|
|
58
|
+
if not value.isdigit():
|
|
59
|
+
return None
|
|
60
|
+
return int(value)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _signature_lineage_index(
|
|
65
|
+
events: List[Event],
|
|
66
|
+
signatures: List[Signature],
|
|
67
|
+
) -> dict[str, list[int]]:
|
|
68
|
+
per_signature_raw_ids: dict[str, list[int]] = {}
|
|
69
|
+
seen = {}
|
|
70
|
+
for event, signature in zip(events, signatures):
|
|
71
|
+
raw_envelope_id = _coerce_raw_envelope_id(getattr(event, "raw_envelope_id", None))
|
|
72
|
+
if raw_envelope_id is None:
|
|
73
|
+
continue
|
|
74
|
+
seen.setdefault(signature.signature_key, set()).add(raw_envelope_id)
|
|
75
|
+
|
|
76
|
+
for key, values in seen.items():
|
|
77
|
+
per_signature_raw_ids[key] = sorted(values)
|
|
78
|
+
return per_signature_raw_ids
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_recurrence_candidates(
|
|
82
|
+
events: List[Event],
|
|
83
|
+
signatures: List[Signature],
|
|
84
|
+
*,
|
|
85
|
+
threshold: int | None = None,
|
|
86
|
+
) -> List[Candidate]:
|
|
87
|
+
runtime_config = get_runtime_config()
|
|
88
|
+
profile = runtime_config.candidate_attention
|
|
89
|
+
if threshold is None:
|
|
90
|
+
threshold = runtime_config.defaults.recurrence_threshold
|
|
91
|
+
|
|
48
92
|
counts = signature_counts(signatures)
|
|
49
93
|
candidates: List[Candidate] = []
|
|
94
|
+
signature_raw_envelope_ids = _signature_lineage_index(events, signatures)
|
|
50
95
|
for signature in signatures:
|
|
51
96
|
count = counts[signature.signature_key]
|
|
52
97
|
if count < threshold:
|
|
53
98
|
continue
|
|
54
|
-
recurrence = min(count /
|
|
55
|
-
recovery =
|
|
56
|
-
spread =
|
|
57
|
-
novelty =
|
|
58
|
-
impact =
|
|
59
|
-
|
|
60
|
-
|
|
99
|
+
recurrence = min(count / float(profile.recurrence_count_normalizer), 1.0)
|
|
100
|
+
recovery = profile.recovery_signal_weight
|
|
101
|
+
spread = profile.spread_signal_weight
|
|
102
|
+
novelty = profile.novelty_signal_weight
|
|
103
|
+
impact = (
|
|
104
|
+
profile.impact_high_weight
|
|
105
|
+
if signature.event_family in {"failure", "auth"}
|
|
106
|
+
else profile.impact_default_weight
|
|
107
|
+
)
|
|
108
|
+
precursor = profile.precursor_weight
|
|
109
|
+
memory_weight = profile.memory_weight
|
|
61
110
|
candidate = score_candidate(
|
|
62
111
|
recurrence=recurrence,
|
|
63
112
|
recovery=recovery,
|
|
@@ -71,7 +120,11 @@ def build_recurrence_candidates(events: List[Event], signatures: List[Signature]
|
|
|
71
120
|
candidate.summary = _candidate_summary(signature, count)
|
|
72
121
|
candidate.source_signature_ids = [signature.signature_key]
|
|
73
122
|
candidate.source_event_ids = [str(i) for i, sig in enumerate(signatures) if sig.signature_key == signature.signature_key]
|
|
74
|
-
candidate.metadata = {
|
|
123
|
+
candidate.metadata = {
|
|
124
|
+
"count": count,
|
|
125
|
+
"service": signature.service,
|
|
126
|
+
"source_raw_envelope_ids": signature_raw_envelope_ids.get(signature.signature_key, []),
|
|
127
|
+
}
|
|
75
128
|
candidates.append(candidate)
|
|
76
129
|
# dedupe by signature key/title
|
|
77
130
|
seen = set()
|
|
@@ -105,6 +158,7 @@ def digest_items(candidates: Iterable[Candidate]) -> List[dict]:
|
|
|
105
158
|
"attention_band": _attention_band(c.decision_band),
|
|
106
159
|
"attention_score": c.score_total,
|
|
107
160
|
"score_total": c.score_total,
|
|
161
|
+
"attention_explanation": _attention_explanation(c),
|
|
108
162
|
"score_breakdown": c.score_breakdown,
|
|
109
163
|
"metadata": c.metadata,
|
|
110
164
|
}
|
package/brainstem/scoring.py
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from .models import Candidate
|
|
4
|
+
from .config import get_runtime_config
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
def decision_band(score_total: float) -> str:
|
|
7
|
-
|
|
8
|
+
profile = get_runtime_config().candidate_attention
|
|
9
|
+
if score_total >= profile.decision_band_promote:
|
|
8
10
|
return "promote_to_incident_memory"
|
|
9
|
-
if score_total >=
|
|
11
|
+
if score_total >= profile.decision_band_urgent_human_review:
|
|
10
12
|
return "urgent_human_review"
|
|
11
|
-
if score_total >=
|
|
13
|
+
if score_total >= profile.decision_band_review:
|
|
12
14
|
return "review"
|
|
13
|
-
if score_total >=
|
|
15
|
+
if score_total >= profile.decision_band_watch:
|
|
14
16
|
return "watch"
|
|
15
17
|
return "ignore"
|
|
16
18
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Any, Callable, Iterable, List, Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
from .adapters import get_raw_input_adapter
|
|
7
|
+
from .models import RawInputEnvelope
|
|
8
|
+
|
|
9
|
+
# Register LogicMonitor adapter definitions for structured ingest support.
|
|
10
|
+
from .connectors import logicmonitor # noqa: F401
|
|
11
|
+
|
|
12
|
+
ErrorHandler = Callable[[Exception, str], None]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def iter_syslog_lines(payload: bytes | str) -> list[str]:
|
|
16
|
+
text = payload.decode("utf-8", errors="replace") if isinstance(payload, (bytes, bytearray)) else str(payload)
|
|
17
|
+
return [line.rstrip("\r") for line in text.splitlines() if line.strip()]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@runtime_checkable
|
|
21
|
+
class SourceDriver(Protocol):
|
|
22
|
+
"""Contract for a small source-to-raw-envelope driver."""
|
|
23
|
+
|
|
24
|
+
source_type: str
|
|
25
|
+
|
|
26
|
+
def parse_payload(
|
|
27
|
+
self,
|
|
28
|
+
payload: Any,
|
|
29
|
+
*,
|
|
30
|
+
tenant_id: str,
|
|
31
|
+
source_path: str = "",
|
|
32
|
+
on_parse_error: ErrorHandler | None = None,
|
|
33
|
+
) -> List[RawInputEnvelope]:
|
|
34
|
+
"""Parse one input payload into one or more raw envelopes."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_SOURCE_DRIVER_REGISTRY: dict[str, SourceDriver] = {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def register_source_driver(driver: SourceDriver) -> None:
|
|
41
|
+
_SOURCE_DRIVER_REGISTRY[driver.source_type] = driver
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def get_source_driver(source_type: str) -> SourceDriver:
|
|
45
|
+
try:
|
|
46
|
+
return _SOURCE_DRIVER_REGISTRY[source_type]
|
|
47
|
+
except KeyError as exc:
|
|
48
|
+
raise ValueError(f"unsupported source_type: {source_type}") from exc
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def list_source_driver_types() -> list[str]:
|
|
52
|
+
return sorted(_SOURCE_DRIVER_REGISTRY.keys())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _iter_payload_items(payload: Any) -> Iterable[Any]:
|
|
56
|
+
if payload is None or isinstance(payload, (bytes, bytearray, str)):
|
|
57
|
+
yield payload
|
|
58
|
+
return
|
|
59
|
+
try:
|
|
60
|
+
iter(payload)
|
|
61
|
+
except TypeError:
|
|
62
|
+
yield payload
|
|
63
|
+
return
|
|
64
|
+
yield from payload
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def parse_source_payloads(
|
|
68
|
+
source_type: str,
|
|
69
|
+
payloads: Any,
|
|
70
|
+
*,
|
|
71
|
+
tenant_id: str,
|
|
72
|
+
source_path: str = "",
|
|
73
|
+
on_parse_error: ErrorHandler | None = None,
|
|
74
|
+
) -> List[RawInputEnvelope]:
|
|
75
|
+
driver = get_source_driver(source_type)
|
|
76
|
+
raw_envelopes: List[RawInputEnvelope] = []
|
|
77
|
+
for payload in _iter_payload_items(payloads):
|
|
78
|
+
raw_envelopes.extend(
|
|
79
|
+
driver.parse_payload(
|
|
80
|
+
payload,
|
|
81
|
+
tenant_id=tenant_id,
|
|
82
|
+
source_path=source_path,
|
|
83
|
+
on_parse_error=on_parse_error,
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
return raw_envelopes
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(frozen=True)
|
|
90
|
+
class FileSourceDriver:
|
|
91
|
+
"""Driver for file-line payloads."""
|
|
92
|
+
|
|
93
|
+
source_type: str = "file"
|
|
94
|
+
|
|
95
|
+
def parse_payload(
|
|
96
|
+
self,
|
|
97
|
+
payload: Any,
|
|
98
|
+
*,
|
|
99
|
+
tenant_id: str,
|
|
100
|
+
source_path: str = "",
|
|
101
|
+
on_parse_error: ErrorHandler | None = None,
|
|
102
|
+
) -> List[RawInputEnvelope]:
|
|
103
|
+
adapter = get_raw_input_adapter(self.source_type)
|
|
104
|
+
text = "" if payload is None else str(payload)
|
|
105
|
+
try:
|
|
106
|
+
return [adapter.parse_raw_input(text, tenant_id=tenant_id, source_path=source_path)]
|
|
107
|
+
except Exception as exc:
|
|
108
|
+
if on_parse_error is None:
|
|
109
|
+
raise
|
|
110
|
+
on_parse_error(exc, text)
|
|
111
|
+
return []
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(frozen=True)
|
|
115
|
+
class SyslogSourceDriver:
|
|
116
|
+
"""Driver for syslog payload chunks and line-oriented payloads."""
|
|
117
|
+
|
|
118
|
+
source_type: str = "syslog"
|
|
119
|
+
|
|
120
|
+
def parse_payload(
|
|
121
|
+
self,
|
|
122
|
+
payload: Any,
|
|
123
|
+
*,
|
|
124
|
+
tenant_id: str,
|
|
125
|
+
source_path: str = "",
|
|
126
|
+
on_parse_error: ErrorHandler | None = None,
|
|
127
|
+
) -> List[RawInputEnvelope]:
|
|
128
|
+
adapter = get_raw_input_adapter(self.source_type)
|
|
129
|
+
raw_envelopes: List[RawInputEnvelope] = []
|
|
130
|
+
|
|
131
|
+
if isinstance(payload, (bytes, bytearray)):
|
|
132
|
+
payload_lines = iter_syslog_lines(payload)
|
|
133
|
+
for line in payload_lines:
|
|
134
|
+
try:
|
|
135
|
+
raw_envelopes.append(adapter.parse_raw_input(line, tenant_id=tenant_id, source_path=source_path))
|
|
136
|
+
except Exception as exc:
|
|
137
|
+
if on_parse_error is None:
|
|
138
|
+
raise
|
|
139
|
+
on_parse_error(exc, line)
|
|
140
|
+
return raw_envelopes
|
|
141
|
+
|
|
142
|
+
payload_text = "" if payload is None else str(payload)
|
|
143
|
+
try:
|
|
144
|
+
raw_envelopes.append(adapter.parse_raw_input(payload_text, tenant_id=tenant_id, source_path=source_path))
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
if on_parse_error is None:
|
|
147
|
+
raise
|
|
148
|
+
on_parse_error(exc, payload_text)
|
|
149
|
+
return raw_envelopes
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True)
|
|
153
|
+
class LogicMonitorSourceDriver:
|
|
154
|
+
"""Driver for narrow LogicMonitor-shaped payloads."""
|
|
155
|
+
|
|
156
|
+
source_type: str = "logicmonitor"
|
|
157
|
+
|
|
158
|
+
def parse_payload(
|
|
159
|
+
self,
|
|
160
|
+
payload: Any,
|
|
161
|
+
*,
|
|
162
|
+
tenant_id: str,
|
|
163
|
+
source_path: str = "",
|
|
164
|
+
on_parse_error: ErrorHandler | None = None,
|
|
165
|
+
) -> List[RawInputEnvelope]:
|
|
166
|
+
adapter = get_raw_input_adapter(self.source_type)
|
|
167
|
+
payload_text = "" if payload is None else str(payload)
|
|
168
|
+
try:
|
|
169
|
+
return [adapter.parse_raw_input(payload, tenant_id=tenant_id, source_path=source_path)]
|
|
170
|
+
except Exception as exc:
|
|
171
|
+
if on_parse_error is None:
|
|
172
|
+
raise
|
|
173
|
+
on_parse_error(exc, payload_text)
|
|
174
|
+
return []
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
register_source_driver(FileSourceDriver())
|
|
178
|
+
register_source_driver(SyslogSourceDriver())
|
|
179
|
+
register_source_driver(LogicMonitorSourceDriver())
|