diff --git a/cmd/server/main.go b/cmd/server/main.go index 9a39433..b6a69bb 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -135,6 +135,7 @@ type componentProbe struct { Status string `json:"status"` LatencyMs int64 `json:"latency_ms"` Error string `json:"error,omitempty"` + Details any `json:"details,omitempty"` } func main() { @@ -444,10 +445,22 @@ func (a *app) probePollErrors(ctx context.Context) componentProbe { return componentProbe{Name: "poll_errors", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()} } if total > 0 { + recent, recentErr := a.recentPollErrors(ctx) status := "degraded" if other > 0 { status = "down" } + details := map[string]any{ + "total": total, + "flood_wait": floodWait, + "unavailable": unavailable, + "other": other, + } + if recentErr != nil { + details["recent_error"] = recentErr.Error() + } else { + details["recent_errors"] = recent + } return componentProbe{ Name: "poll_errors", Status: status, @@ -456,11 +469,64 @@ func (a *app) probePollErrors(ctx context.Context) componentProbe { " flood_wait=" + strconv.FormatInt(floodWait, 10) + " unavailable=" + strconv.FormatInt(unavailable, 10) + " other=" + strconv.FormatInt(other, 10), + Details: details, } } return componentProbe{Name: "poll_errors", Status: "ok", LatencyMs: time.Since(start).Milliseconds()} } +func (a *app) recentPollErrors(ctx context.Context) ([]map[string]any, error) { + rows, err := a.db.Query(ctx, ` + SELECT + c.id, + c.identifier, + COALESCE(c.title, '') AS title, + s.slug, + s.title AS section_title, + COALESCE(c.last_poll_error_code, '') AS error_code, + COALESCE(c.last_poll_error, '') AS error_text, + c.last_poll_error_at + FROM channels c + LEFT JOIN sections s ON s.id = c.section_id + WHERE c.is_active = true + AND c.source_channel_id IS NULL + AND c.last_poll_status = 'error' + ORDER BY c.last_poll_error_at DESC NULLS LAST, c.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 + identifier string + title string + sectionSlug sql.NullString + sectionTitle sql.NullString + code string + text string + at sql.NullTime + ) + if err := rows.Scan(&id, &identifier, &title, §ionSlug, §ionTitle, &code, &text, &at); err != nil { + return nil, err + } + out = append(out, map[string]any{ + "channel_id": id, + "identifier": identifier, + "title": nullableString(title), + "section_slug": nullString(sectionSlug), + "section_title": nullString(sectionTitle), + "error_code": nullableString(code), + "error": nullableString(text), + "error_at": nullTime(at), + }) + } + return out, rows.Err() +} + func (a *app) probeMediaStorage(ctx context.Context) componentProbe { start := time.Now() if a.minio == nil || a.cfg.MinioBucket == "" {