fix(syslog): a device through Docker's address stays one source when that address changes (#6717)
Docker's ingress address depends on the node Steward's task is on, and sources were keyed by (address, sender). After a task move the same device arrived under a new key and became a new "offered" row, so its lines were counted, not kept. A named sender is now one row whatever address it last came from, enforced by a partial unique index. Migration syslog_005 folds rows sharing a name into the added one and moves their lines and patterns across. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"""Syslog: one row per name at Docker's address, whichever address it last came
|
||||
from. Rows sharing a name fold into the added one, or else the newest.
|
||||
|
||||
Revision ID: syslog_005_one_row_per_sender
|
||||
Revises: syslog_004_device_address
|
||||
Create Date: 2026-10-30
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "syslog_005_one_row_per_sender"
|
||||
down_revision: Union[str, None] = "syslog_004_device_address"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
_STATE_FIRST = {"accepted": 0, "ignored": 1, "offered": 2}
|
||||
|
||||
|
||||
def _fold(bind, sender: str) -> None:
|
||||
rows = bind.execute(sa.text(
|
||||
"SELECT id, address, state, device_address, host_id, hostname_seen, first_seen_at, last_seen_at, "
|
||||
"lines_total, last_severity, last_line FROM syslog_sources WHERE sender = :sender"
|
||||
), {"sender": sender}).mappings().all()
|
||||
keep = min(rows, key=lambda r: (_STATE_FIRST.get(r["state"], 3), -r["last_seen_at"].timestamp()))
|
||||
newest = max(rows, key=lambda r: r["last_seen_at"])
|
||||
others = [r["id"] for r in rows if r["id"] != keep["id"]]
|
||||
in_others = sa.bindparam("others", expanding=True)
|
||||
for table in ("syslog_lines", "syslog_patterns"):
|
||||
bind.execute(sa.text(f"UPDATE {table} SET source_id = :keep WHERE source_id IN :others").bindparams(in_others),
|
||||
{"keep": keep["id"], "others": others})
|
||||
bind.execute(sa.text("DELETE FROM syslog_sources WHERE id IN :others").bindparams(in_others), {"others": others})
|
||||
bind.execute(sa.text(
|
||||
"UPDATE syslog_sources SET address = :address, device_address = :device_address, host_id = :host_id, "
|
||||
"hostname_seen = :hostname_seen, first_seen_at = :first_seen_at, last_seen_at = :last_seen_at, "
|
||||
"lines_total = :lines_total, last_severity = :last_severity, last_line = :last_line WHERE id = :id"
|
||||
), {
|
||||
"id": keep["id"],
|
||||
"address": newest["address"],
|
||||
"device_address": next((r["device_address"] for r in rows if r["device_address"]), ""),
|
||||
"host_id": keep["host_id"] or next((r["host_id"] for r in rows if r["host_id"]), None),
|
||||
"hostname_seen": newest["hostname_seen"],
|
||||
"first_seen_at": min(r["first_seen_at"] for r in rows),
|
||||
"last_seen_at": newest["last_seen_at"],
|
||||
"lines_total": sum(r["lines_total"] or 0 for r in rows),
|
||||
"last_severity": newest["last_severity"],
|
||||
"last_line": newest["last_line"],
|
||||
})
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
bind = op.get_bind()
|
||||
senders = bind.execute(sa.text(
|
||||
"SELECT sender FROM syslog_sources WHERE sender <> '' GROUP BY sender HAVING count(*) > 1"
|
||||
)).scalars().all()
|
||||
for sender in senders:
|
||||
_fold(bind, sender)
|
||||
op.create_index("uq_syslog_sources_sender", "syslog_sources", ["sender"], unique=True,
|
||||
postgresql_where=sa.text("sender <> ''"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("uq_syslog_sources_sender", table_name="syslog_sources")
|
||||
Reference in New Issue
Block a user