feat: expose monitoring tg media repair details
This commit is contained in:
@@ -558,17 +558,81 @@ func (a *app) probeMediaMetadata(ctx context.Context) componentProbe {
|
|||||||
return componentProbe{Name: "media_metadata", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
|
return componentProbe{Name: "media_metadata", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
|
||||||
}
|
}
|
||||||
if missingFiles > 0 {
|
if missingFiles > 0 {
|
||||||
|
recent, recentErr := a.recentMissingMedia(ctx)
|
||||||
|
details := map[string]any{
|
||||||
|
"messages_with_media": withMedia,
|
||||||
|
"missing_media_files": missingFiles,
|
||||||
|
}
|
||||||
|
if recentErr != nil {
|
||||||
|
details["recent_error"] = recentErr.Error()
|
||||||
|
} else {
|
||||||
|
details["recent_missing_media"] = recent
|
||||||
|
}
|
||||||
return componentProbe{
|
return componentProbe{
|
||||||
Name: "media_metadata",
|
Name: "media_metadata",
|
||||||
Status: "down",
|
Status: "down",
|
||||||
LatencyMs: time.Since(start).Milliseconds(),
|
LatencyMs: time.Since(start).Milliseconds(),
|
||||||
Error: "messages_with_media=" + strconv.FormatInt(withMedia, 10) +
|
Error: "messages_with_media=" + strconv.FormatInt(withMedia, 10) +
|
||||||
" missing_media_files=" + strconv.FormatInt(missingFiles, 10),
|
" missing_media_files=" + strconv.FormatInt(missingFiles, 10),
|
||||||
|
Details: details,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return componentProbe{Name: "media_metadata", Status: "ok", LatencyMs: time.Since(start).Milliseconds()}
|
return componentProbe{Name: "media_metadata", Status: "ok", LatencyMs: time.Since(start).Milliseconds()}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *app) recentMissingMedia(ctx context.Context) ([]map[string]any, error) {
|
||||||
|
rows, err := a.db.Query(ctx, `
|
||||||
|
SELECT
|
||||||
|
m.id,
|
||||||
|
m.tg_message_id,
|
||||||
|
m.date,
|
||||||
|
c.id AS channel_id,
|
||||||
|
c.identifier,
|
||||||
|
COALESCE(c.title, '') AS channel_title,
|
||||||
|
s.slug,
|
||||||
|
s.title AS section_title
|
||||||
|
FROM messages m
|
||||||
|
JOIN channels c ON c.id = m.channel_id
|
||||||
|
LEFT JOIN sections s ON s.id = c.section_id
|
||||||
|
WHERE m.has_media = true
|
||||||
|
AND jsonb_array_length(COALESCE(m.media_files, '[]'::jsonb)) = 0
|
||||||
|
ORDER BY m.date DESC, m.id DESC
|
||||||
|
LIMIT 5`)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
out := make([]map[string]any, 0, 5)
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
id int64
|
||||||
|
tgMessageID int64
|
||||||
|
date time.Time
|
||||||
|
channelID int64
|
||||||
|
identifier string
|
||||||
|
channelTitle string
|
||||||
|
sectionSlug sql.NullString
|
||||||
|
sectionTitle sql.NullString
|
||||||
|
)
|
||||||
|
if err := rows.Scan(&id, &tgMessageID, &date, &channelID, &identifier, &channelTitle, §ionSlug, §ionTitle); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, map[string]any{
|
||||||
|
"message_id": id,
|
||||||
|
"tg_message_id": tgMessageID,
|
||||||
|
"message_date": date,
|
||||||
|
"channel_id": channelID,
|
||||||
|
"identifier": identifier,
|
||||||
|
"channel_title": nullableString(channelTitle),
|
||||||
|
"section_slug": nullString(sectionSlug),
|
||||||
|
"section_title": nullString(sectionTitle),
|
||||||
|
"repair_action": "/api/v1/channels/" + strconv.FormatInt(channelID, 10) + "/backfill-media",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
func (a *app) handleAccessMe(w http.ResponseWriter, r *http.Request) {
|
func (a *app) handleAccessMe(w http.ResponseWriter, r *http.Request) {
|
||||||
scope := readAccess(r)
|
scope := readAccess(r)
|
||||||
writeJSON(w, http.StatusOK, map[string]any{
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from datetime import datetime, timezone
|
|||||||
|
|
||||||
import structlog
|
import structlog
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
from sqlalchemy import func, select
|
from sqlalchemy import func, or_, select
|
||||||
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
from sqlalchemy.dialects.postgresql import insert as pg_insert
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
|
|
||||||
@@ -262,10 +262,15 @@ async def backfill_media(channel_id: int, batch_size: int = 50) -> dict[str, int
|
|||||||
if channel is None:
|
if channel is None:
|
||||||
raise RuntimeError("channel not found")
|
raise RuntimeError("channel not found")
|
||||||
|
|
||||||
|
missing_media_condition = or_(
|
||||||
|
Message.media_files.is_(None),
|
||||||
|
func.jsonb_array_length(Message.media_files) == 0,
|
||||||
|
)
|
||||||
|
|
||||||
pending_q = select(func.count(Message.id)).where(
|
pending_q = select(func.count(Message.id)).where(
|
||||||
Message.channel_id == channel_id,
|
Message.channel_id == channel_id,
|
||||||
Message.has_media.is_(True),
|
Message.has_media.is_(True),
|
||||||
Message.media_files.is_(None),
|
missing_media_condition,
|
||||||
)
|
)
|
||||||
pending_total = (await session.execute(pending_q)).scalar_one()
|
pending_total = (await session.execute(pending_q)).scalar_one()
|
||||||
|
|
||||||
@@ -275,7 +280,7 @@ async def backfill_media(channel_id: int, batch_size: int = 50) -> dict[str, int
|
|||||||
.where(
|
.where(
|
||||||
Message.channel_id == channel_id,
|
Message.channel_id == channel_id,
|
||||||
Message.has_media.is_(True),
|
Message.has_media.is_(True),
|
||||||
Message.media_files.is_(None),
|
missing_media_condition,
|
||||||
)
|
)
|
||||||
.order_by(Message.tg_message_id.asc())
|
.order_by(Message.tg_message_id.asc())
|
||||||
.limit(batch_size)
|
.limit(batch_size)
|
||||||
|
|||||||
Reference in New Issue
Block a user