fix: alert ingestion broken under Pydantic 2 (silent _-prefix drop) + better logs (#858)
Two related fixes — the underlying bug is also why "better logs" was requested. The reporter's symptom: ERROR | app.incidents.routes.incident_alert:create_alert_auto_route:278 Failed to create alert ... 'GenericAlertModel' object has no attribute '_source' Root cause: Pydantic 2 (which we migrated to in #849) treats names beginning with an underscore as PrivateAttr declarations and silently drops them from input parsing. GenericAlertModel was defined with `_index: str`, `_id: str`, `_version: int`, `_source: GenericSourceModel` to match Elasticsearch's JSON shape. Under Pydantic 1 those were normal fields; under Pydantic 2 every one of them gets stripped at parse time, leaving an empty model. Downstream code then accesses `alert_details._source.process_id` and gets AttributeError because the object has no `_source` attribute (private attrs aren't even discoverable via getattr unless the model declared them as PrivateAttr explicitly, which it didn't). Three places in the codebase had this exact bug: - backend/app/incidents/schema/incident_alert.py:GenericAlertModel (the active one — flowing through every Graylog event ingestion) - backend/app/integrations/alert_escalation/schema/escalate_alert.py:GenericAlertModel (currently no live imports — only commented in ask_socfortress — but defined and broken in the same way) - backend/app/agents/velociraptor/schema/agents.py:Organization (the `_client_config: ClientConfig` field, never accessed in the codebase so no observable bug, but fixed for consistency) The fix uses Pydantic-2-idiomatic `Field(alias="_index")` so the Elasticsearch-shaped JSON keys still parse, while the Python attributes are exposed under their non-underscore names. `populate_by_name=True` is added to model_config so input still accepts both shapes during any transition. Code changes: - 3 model definitions: rename the private-looking fields and add Field(alias=...) + populate_by_name=True - 10 accessor sites updated: backend/app/incidents/services/incident_alert.py (8 sites) backend/app/threat_intel/routes/socfortress.py (2 sites) All replace `alert_details._source/_index/_id/_version` → `alert_details.source/index/id/version`. Better error logging (the original ask): - The `except Exception as e: logger.error(f"... {e}")` in create_alert_auto_route now branches: - For ValidationError, logs each field-level error with location path, error type, message, and the offending input (truncated). - For anything else, uses logger.opt(exception=True).error to attach the full traceback so AttributeError-class bugs (like this one was) are immediately diagnosable from logs. Verified locally: - GenericAlertModel parses an Elasticsearch-shaped dict and exposes the expected fields: m = GenericAlertModel(**{"_index":"i","_id":"a","_version":1, "_source":{"agent_name":"x","timestamp":"t"}}) m.index == "i"; m.source.agent_name == "x" - Validation error path produces the structured per-field log: Caught 4 validation errors: field=_id type=missing msg=Field required input={...} field=_version type=int_parsing msg=... input='not-an-int' field=_source.timestamp type=missing msg=Field required input={...} field=_source.agent_name type=string_type msg=... input=42 - Fresh deploy: backend boots clean, admin login HTTP 200, no regressions Co-authored-by: taylor_socfortress <taylor.walton@socfortress.co> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>