feat: expose monitoring tg poll diagnostics

This commit is contained in:
Grendgi
2026-06-17 17:19:13 +03:00
parent bd3b54dc7d
commit 696b7eda6f

View File

@@ -135,6 +135,7 @@ type componentProbe struct {
Status string `json:"status"` Status string `json:"status"`
LatencyMs int64 `json:"latency_ms"` LatencyMs int64 `json:"latency_ms"`
Error string `json:"error,omitempty"` Error string `json:"error,omitempty"`
Details any `json:"details,omitempty"`
} }
func main() { 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()} return componentProbe{Name: "poll_errors", Status: "down", LatencyMs: time.Since(start).Milliseconds(), Error: err.Error()}
} }
if total > 0 { if total > 0 {
recent, recentErr := a.recentPollErrors(ctx)
status := "degraded" status := "degraded"
if other > 0 { if other > 0 {
status = "down" 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{ return componentProbe{
Name: "poll_errors", Name: "poll_errors",
Status: status, Status: status,
@@ -456,11 +469,64 @@ func (a *app) probePollErrors(ctx context.Context) componentProbe {
" flood_wait=" + strconv.FormatInt(floodWait, 10) + " flood_wait=" + strconv.FormatInt(floodWait, 10) +
" unavailable=" + strconv.FormatInt(unavailable, 10) + " unavailable=" + strconv.FormatInt(unavailable, 10) +
" other=" + strconv.FormatInt(other, 10), " other=" + strconv.FormatInt(other, 10),
Details: details,
} }
} }
return componentProbe{Name: "poll_errors", Status: "ok", LatencyMs: time.Since(start).Milliseconds()} 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, &sectionSlug, &sectionTitle, &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 { func (a *app) probeMediaStorage(ctx context.Context) componentProbe {
start := time.Now() start := time.Now()
if a.minio == nil || a.cfg.MinioBucket == "" { if a.minio == nil || a.cfg.MinioBucket == "" {