Added the field-baseline storage layer.

This commit is contained in:
larssand
2026-06-22 21:18:17 +02:00
parent 0bc51bdd0d
commit d94cb20987
3 changed files with 35 additions and 1 deletions

View File

@@ -39,6 +39,11 @@ class BaselineStore:
source_ip text not null, kind text not null, value text not null,
seen_count integer not null, primary key (source_ip, kind, value)
);
create table if not exists profile_buckets (
stream_id text not null, entity text not null, field text not null, bucket_start integer not null,
events integer not null, numeric_sum real not null, numeric_sum_squares real not null,
primary key (stream_id, entity, field, bucket_start)
);
"""
)
@@ -81,6 +86,33 @@ class BaselineStore:
)
return inserted
def ingest_profile_fields(self, events: list[LogEvent], profiles: dict[str, object], *, observed_at: int | None = None) -> int:
observed_at = observed_at or int(time.time())
bucket = observed_at - (observed_at % self.bucket_seconds)
pending: dict[tuple[str, str, str], list[float]] = defaultdict(lambda: [0, 0.0, 0.0])
for event in events:
stream_id = event.fields.get("fgai_stream_id", "")
profile = profiles.get(stream_id)
if not profile:
continue
entity_field = str(getattr(profile, "entity_field", "")).lower()
entity = event.fields.get(entity_field)
if not entity:
continue
fields = [*getattr(profile, "categorical_fields", ()), *getattr(profile, "numeric_fields", ())]
numeric = {str(field).lower() for field in getattr(profile, "numeric_fields", ())}
for field in fields:
key = (stream_id, entity, str(field).lower())
value = _number(event.fields.get(key[2])) if key[2] in numeric else 0
pending[key][0] += 1
pending[key][1] += value
pending[key][2] += value * value
with self._connect() as connection:
for (stream_id, entity, field), values in pending.items():
connection.execute("""insert into profile_buckets values (?, ?, ?, ?, ?, ?, ?)
on conflict(stream_id, entity, field, bucket_start) do update set events=events+excluded.events,numeric_sum=numeric_sum+excluded.numeric_sum,numeric_sum_squares=numeric_sum_squares+excluded.numeric_sum_squares""", (stream_id, entity, field, bucket, *values))
return len(pending)
def profiles(self, source_ips: set[str]) -> dict[str, dict[str, object]]:
profiles: dict[str, dict[str, object]] = {}
with self._connect() as connection: