The log now has a stored, queryable copy for the diagnostics console: every WARNING and above plus the request line of every request, kept for log_retention_days (default 7, no "forever") and pruned hourly by the new log_gc job. Rows are built on the logging thread from the same redacted fields as the stdout line, queued without blocking and written in batches by one background thread on its own connection. A full queue or a failed write drops and reports instead of retrying. Flood control keeps 30 of any one message per minute plus a count of the rest; anonymous request lines are capped at 60 a minute per client, and the client address is never stored. Deleting an account deletes its stored events. Measured on prod first: a YouTube RSS outage wrote 11k warnings in four hours, one per channel per poll. The poll now writes one warning with per-reason counts and a few examples, and a failure on our side gets one traceback per poll instead of one per channel.
49 lines
2.0 KiB
Python
49 lines
2.0 KiB
Python
"""add log_events — stored log events for diagnostics (E22 S3)
|
|
|
|
Revision ID: 0086_log_events
|
|
Revises: 0085_users_last_login_at
|
|
Create Date: 2026-09-17
|
|
|
|
Written by app/logsink.py's background writer (WARNING and above, request lines), pruned by the log_gc
|
|
job after `log_retention_days`. Sized against prod on 2026-09-16: ~2k rows on a normal day, ~14k on a
|
|
YouTube-RSS-outage day before flood control — a few MB over the 7-day window. `user_id` carries no
|
|
foreign key on purpose (see models.LogEvent).
|
|
"""
|
|
|
|
import sqlalchemy as sa
|
|
from alembic import op
|
|
from sqlalchemy.dialects import postgresql
|
|
|
|
revision: str = "0086_log_events"
|
|
down_revision: str | None = "0085_users_last_login_at"
|
|
branch_labels: str | None = None
|
|
depends_on: str | None = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
op.create_table(
|
|
"log_events",
|
|
sa.Column("id", sa.BigInteger(), primary_key=True),
|
|
sa.Column("ts", sa.DateTime(timezone=True), nullable=False),
|
|
sa.Column("level", sa.String(10), nullable=False),
|
|
sa.Column("sev", sa.SmallInteger(), nullable=False),
|
|
sa.Column("source", sa.String(16), nullable=False),
|
|
sa.Column("logger", sa.String(128), nullable=False),
|
|
sa.Column("event", sa.String(64), nullable=True),
|
|
sa.Column("msg", sa.Text(), nullable=False),
|
|
sa.Column("attrs", postgresql.JSONB(), nullable=True),
|
|
sa.Column("exc", sa.Text(), nullable=True),
|
|
sa.Column("request_id", sa.String(64), nullable=True),
|
|
sa.Column("user_id", sa.Integer(), nullable=True),
|
|
sa.Column("debug_session_id", sa.String(64), nullable=True),
|
|
sa.Column("job_id", sa.Integer(), nullable=True),
|
|
sa.Column("version", sa.String(64), nullable=True),
|
|
)
|
|
op.create_index("ix_log_events_ts", "log_events", ["ts"])
|
|
op.create_index("ix_log_events_request_id", "log_events", ["request_id"])
|
|
op.create_index("ix_log_events_user_ts", "log_events", ["user_id", "ts"])
|
|
|
|
|
|
def downgrade() -> None:
|
|
op.drop_table("log_events")
|