From 52a54f9c5cc33c8832b3090c23d3d085cd4623a6 Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Fri, 15 May 2026 10:53:38 -0400 Subject: [PATCH 1/2] feat(alert): add Telegram, PagerDuty, Pushover, Gotify providers Expand alert provider count from 5 to 9. All new providers use the shared HTTPProvider with closure-based payload functions. Includes TUI form support and tests for each provider. --- internal/alert/alert.go | 76 ++++++++++++++++++++ internal/alert/alert_test.go | 104 +++++++++++++++++++++++++++ internal/tui/tab_alerts.go | 131 ++++++++++++++++++++++++++++++++++- 3 files changed, 308 insertions(+), 3 deletions(-) diff --git a/internal/alert/alert.go b/internal/alert/alert.go index d67a30d..c60013f 100644 --- a/internal/alert/alert.go +++ b/internal/alert/alert.go @@ -7,6 +7,7 @@ import ( "go-upkeep/internal/models" "net/http" "net/smtp" + "strconv" "strings" "time" ) @@ -52,6 +53,52 @@ func webhookPayload(title, message string) ([]byte, error) { return json.Marshal(map[string]string{"title": title, "message": message, "status": "alert"}) } +func telegramPayload(chatID string) PayloadFunc { + return func(title, message string) ([]byte, error) { + return json.Marshal(map[string]string{ + "chat_id": chatID, + "text": fmt.Sprintf("*%s*\n%s", title, message), + "parse_mode": "Markdown", + }) + } +} + +func pagerdutyPayload(routingKey, severity string) PayloadFunc { + return func(title, message string) ([]byte, error) { + return json.Marshal(map[string]any{ + "routing_key": routingKey, + "event_action": "trigger", + "payload": map[string]string{ + "summary": fmt.Sprintf("%s: %s", title, message), + "source": "go-upkeep", + "severity": severity, + }, + }) + } +} + +func pushoverPayload(token, user string) PayloadFunc { + return func(title, message string) ([]byte, error) { + return json.Marshal(map[string]string{ + "token": token, + "user": user, + "title": title, + "message": message, + }) + } +} + +func gotifyPayload(priority string) PayloadFunc { + return func(title, message string) ([]byte, error) { + pri, _ := strconv.Atoi(priority) + return json.Marshal(map[string]any{ + "title": title, + "message": message, + "priority": pri, + }) + } +} + func GetProvider(cfg models.AlertConfig) Provider { switch cfg.Type { case "discord": @@ -85,6 +132,35 @@ func GetProvider(cfg models.AlertConfig) Provider { Username: cfg.Settings["username"], Password: cfg.Settings["password"], } + case "telegram": + return &HTTPProvider{ + URL: fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", cfg.Settings["token"]), + Payload: telegramPayload(cfg.Settings["chat_id"]), + } + case "pagerduty": + severity := "critical" + if s, ok := cfg.Settings["severity"]; ok && s != "" { + severity = s + } + return &HTTPProvider{ + URL: "https://events.pagerduty.com/v2/enqueue", + Payload: pagerdutyPayload(cfg.Settings["routing_key"], severity), + } + case "pushover": + return &HTTPProvider{ + URL: "https://api.pushover.net/1/messages.json", + Payload: pushoverPayload(cfg.Settings["token"], cfg.Settings["user"]), + } + case "gotify": + priority := "5" + if p, ok := cfg.Settings["priority"]; ok && p != "" { + priority = p + } + serverURL := strings.TrimRight(cfg.Settings["url"], "/") + return &HTTPProvider{ + URL: fmt.Sprintf("%s/message?token=%s", serverURL, cfg.Settings["token"]), + Payload: gotifyPayload(priority), + } default: return nil } diff --git a/internal/alert/alert_test.go b/internal/alert/alert_test.go index 348f2c9..35e1c8d 100644 --- a/internal/alert/alert_test.go +++ b/internal/alert/alert_test.go @@ -101,6 +101,110 @@ func TestNtfyProvider(t *testing.T) { } } +func TestHTTPProviderTelegram(t *testing.T) { + var received map[string]string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewDecoder(r.Body).Decode(&received) + w.WriteHeader(200) + })) + defer srv.Close() + + p := &HTTPProvider{URL: srv.URL, Payload: telegramPayload("12345")} + if err := p.Send("Alert", "Down"); err != nil { + t.Fatalf("Send: %v", err) + } + if received["chat_id"] != "12345" { + t.Errorf("expected chat_id '12345', got '%s'", received["chat_id"]) + } + if received["text"] != "*Alert*\nDown" { + t.Errorf("unexpected text: %s", received["text"]) + } + if received["parse_mode"] != "Markdown" { + t.Errorf("expected parse_mode 'Markdown', got '%s'", received["parse_mode"]) + } +} + +func TestHTTPProviderPagerDuty(t *testing.T) { + var received map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewDecoder(r.Body).Decode(&received) + w.WriteHeader(200) + })) + defer srv.Close() + + p := &HTTPProvider{URL: srv.URL, Payload: pagerdutyPayload("test-key", "critical")} + if err := p.Send("Alert", "Down"); err != nil { + t.Fatalf("Send: %v", err) + } + if received["routing_key"] != "test-key" { + t.Errorf("expected routing_key 'test-key', got '%v'", received["routing_key"]) + } + if received["event_action"] != "trigger" { + t.Errorf("expected event_action 'trigger', got '%v'", received["event_action"]) + } + payload := received["payload"].(map[string]any) + if payload["summary"] != "Alert: Down" { + t.Errorf("unexpected summary: %v", payload["summary"]) + } + if payload["severity"] != "critical" { + t.Errorf("expected severity 'critical', got '%v'", payload["severity"]) + } +} + +func TestHTTPProviderPushover(t *testing.T) { + var received map[string]string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewDecoder(r.Body).Decode(&received) + w.WriteHeader(200) + })) + defer srv.Close() + + p := &HTTPProvider{URL: srv.URL, Payload: pushoverPayload("app-tok", "user-key")} + if err := p.Send("Alert", "Down"); err != nil { + t.Fatalf("Send: %v", err) + } + if received["token"] != "app-tok" { + t.Errorf("expected token 'app-tok', got '%s'", received["token"]) + } + if received["user"] != "user-key" { + t.Errorf("expected user 'user-key', got '%s'", received["user"]) + } + if received["title"] != "Alert" || received["message"] != "Down" { + t.Errorf("unexpected payload: %v", received) + } +} + +func TestHTTPProviderGotify(t *testing.T) { + var received map[string]any + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + json.NewDecoder(r.Body).Decode(&received) + w.WriteHeader(200) + })) + defer srv.Close() + + p := &HTTPProvider{URL: srv.URL, Payload: gotifyPayload("8")} + if err := p.Send("Alert", "Down"); err != nil { + t.Fatalf("Send: %v", err) + } + if received["title"] != "Alert" || received["message"] != "Down" { + t.Errorf("unexpected payload: %v", received) + } + if pri, ok := received["priority"].(float64); !ok || pri != 8 { + t.Errorf("expected priority 8, got %v", received["priority"]) + } +} + +func TestGetProviderNewTypes(t *testing.T) { + for _, typ := range []string{"telegram", "pagerduty", "pushover", "gotify"} { + p := GetProvider(models.AlertConfig{Type: typ, Settings: map[string]string{ + "token": "x", "chat_id": "1", "routing_key": "k", "user": "u", "url": "http://localhost", + }}) + if p == nil { + t.Errorf("GetProvider(%q) returned nil", typ) + } + } +} + func TestGetProviderUnknown(t *testing.T) { p := GetProvider(models.AlertConfig{Type: "unknown"}) if p != nil { diff --git a/internal/tui/tab_alerts.go b/internal/tui/tab_alerts.go index 11c9bf6..342e1bd 100644 --- a/internal/tui/tab_alerts.go +++ b/internal/tui/tab_alerts.go @@ -23,6 +23,19 @@ type alertFormData struct { NtfyUser string NtfyPass string NtfyPri string + // Telegram + TelegramToken string + TelegramChatID string + // PagerDuty + PagerDutyKey string + PagerDutySeverity string + // Pushover + PushoverToken string + PushoverUser string + // Gotify + GotifyURL string + GotifyToken string + GotifyPriority string } func fmtAlertType(t string) string { @@ -37,6 +50,14 @@ func fmtAlertType(t string) string { return lipgloss.NewStyle().Foreground(lipgloss.Color("#73F59F")).Render(t) case "ntfy": return lipgloss.NewStyle().Foreground(lipgloss.Color("#FF6B6B")).Render(t) + case "telegram": + return lipgloss.NewStyle().Foreground(lipgloss.Color("#26A5E4")).Render(t) + case "pagerduty": + return lipgloss.NewStyle().Foreground(lipgloss.Color("#06AC38")).Render(t) + case "pushover": + return lipgloss.NewStyle().Foreground(lipgloss.Color("#249DF1")).Render(t) + case "gotify": + return lipgloss.NewStyle().Foreground(lipgloss.Color("#3F8BBA")).Render(t) default: return t } @@ -64,6 +85,26 @@ func fmtAlertConfig(alert struct { return limitStr(fmt.Sprintf("%s/%s", url, topic), 34) } return subtleStyle.Render("—") + case "telegram": + if id := alert.Settings["chat_id"]; id != "" { + return limitStr(fmt.Sprintf("chat:%s", id), 34) + } + return subtleStyle.Render("—") + case "pagerduty": + if key := alert.Settings["routing_key"]; key != "" { + return limitStr(key, 34) + } + return subtleStyle.Render("—") + case "pushover": + if user := alert.Settings["user"]; user != "" { + return limitStr(fmt.Sprintf("user:%s", user), 34) + } + return subtleStyle.Render("—") + case "gotify": + if url := alert.Settings["url"]; url != "" { + return limitStr(url, 34) + } + return subtleStyle.Render("—") default: if val, ok := alert.Settings["url"]; ok { return limitStr(val, 34) @@ -102,8 +143,10 @@ func (m Model) viewAlertsTab() string { func (m *Model) initAlertHuhForm() tea.Cmd { m.alertFormData = &alertFormData{ - AlertType: "discord", - NtfyPri: "3", + AlertType: "discord", + NtfyPri: "3", + PagerDutySeverity: "critical", + GotifyPriority: "5", } if m.editID > 0 { @@ -128,6 +171,19 @@ func (m *Model) initAlertHuhForm() tea.Cmd { m.alertFormData.NtfyUser = alert.Settings["username"] m.alertFormData.NtfyPass = alert.Settings["password"] m.alertFormData.NtfyPri = alert.Settings["priority"] + case "telegram": + m.alertFormData.TelegramToken = alert.Settings["token"] + m.alertFormData.TelegramChatID = alert.Settings["chat_id"] + case "pagerduty": + m.alertFormData.PagerDutyKey = alert.Settings["routing_key"] + m.alertFormData.PagerDutySeverity = alert.Settings["severity"] + case "pushover": + m.alertFormData.PushoverToken = alert.Settings["token"] + m.alertFormData.PushoverUser = alert.Settings["user"] + case "gotify": + m.alertFormData.GotifyURL = alert.Settings["url"] + m.alertFormData.GotifyToken = alert.Settings["token"] + m.alertFormData.GotifyPriority = alert.Settings["priority"] } break } @@ -152,6 +208,10 @@ func (m *Model) initAlertHuhForm() tea.Cmd { huh.NewOption("Webhook", "webhook"), huh.NewOption("Email (SMTP)", "email"), huh.NewOption("Ntfy", "ntfy"), + huh.NewOption("Telegram", "telegram"), + huh.NewOption("PagerDuty", "pagerduty"), + huh.NewOption("Pushover", "pushover"), + huh.NewOption("Gotify", "gotify"), ).Value(&m.alertFormData.AlertType), ).Title("Alert Config"), huh.NewGroup( @@ -159,7 +219,8 @@ func (m *Model) initAlertHuhForm() tea.Cmd { Placeholder("https://discord.com/api/webhooks/..."). Value(&m.alertFormData.WebhookURL), ).Title("Webhook").WithHideFunc(func() bool { - return m.alertFormData.AlertType == "email" || m.alertFormData.AlertType == "ntfy" + t := m.alertFormData.AlertType + return t != "discord" && t != "slack" && t != "webhook" }), huh.NewGroup( huh.NewInput().Title("Ntfy Server URL"). @@ -207,6 +268,57 @@ func (m *Model) initAlertHuhForm() tea.Cmd { ).Title("Email Settings").WithHideFunc(func() bool { return m.alertFormData.AlertType != "email" }), + huh.NewGroup( + huh.NewInput().Title("Bot Token"). + Placeholder("123456:ABC-DEF1234..."). + Value(&m.alertFormData.TelegramToken), + huh.NewInput().Title("Chat ID"). + Placeholder("-1001234567890"). + Value(&m.alertFormData.TelegramChatID), + ).Title("Telegram Settings").WithHideFunc(func() bool { + return m.alertFormData.AlertType != "telegram" + }), + huh.NewGroup( + huh.NewInput().Title("Routing Key"). + Placeholder("your-integration-routing-key"). + Value(&m.alertFormData.PagerDutyKey), + huh.NewSelect[string]().Title("Severity"). + Options( + huh.NewOption("Critical", "critical"), + huh.NewOption("Error", "error"), + huh.NewOption("Warning", "warning"), + huh.NewOption("Info", "info"), + ).Value(&m.alertFormData.PagerDutySeverity), + ).Title("PagerDuty Settings").WithHideFunc(func() bool { + return m.alertFormData.AlertType != "pagerduty" + }), + huh.NewGroup( + huh.NewInput().Title("App Token"). + Placeholder("your-pushover-app-token"). + Value(&m.alertFormData.PushoverToken), + huh.NewInput().Title("User Key"). + Placeholder("your-pushover-user-key"). + Value(&m.alertFormData.PushoverUser), + ).Title("Pushover Settings").WithHideFunc(func() bool { + return m.alertFormData.AlertType != "pushover" + }), + huh.NewGroup( + huh.NewInput().Title("Server URL"). + Placeholder("https://gotify.example.com"). + Value(&m.alertFormData.GotifyURL), + huh.NewInput().Title("App Token"). + Placeholder("your-gotify-app-token"). + Value(&m.alertFormData.GotifyToken), + huh.NewSelect[string]().Title("Priority"). + Options( + huh.NewOption("Min (0)", "0"), + huh.NewOption("Low (2)", "2"), + huh.NewOption("Normal (5)", "5"), + huh.NewOption("High (8)", "8"), + ).Value(&m.alertFormData.GotifyPriority), + ).Title("Gotify Settings").WithHideFunc(func() bool { + return m.alertFormData.AlertType != "gotify" + }), ).WithTheme(huh.ThemeDracula()) return m.huhForm.Init() @@ -230,6 +342,19 @@ func (m *Model) submitAlertForm() { settings["priority"] = d.NtfyPri settings["username"] = d.NtfyUser settings["password"] = d.NtfyPass + case "telegram": + settings["token"] = d.TelegramToken + settings["chat_id"] = d.TelegramChatID + case "pagerduty": + settings["routing_key"] = d.PagerDutyKey + settings["severity"] = d.PagerDutySeverity + case "pushover": + settings["token"] = d.PushoverToken + settings["user"] = d.PushoverUser + case "gotify": + settings["url"] = d.GotifyURL + settings["token"] = d.GotifyToken + settings["priority"] = d.GotifyPriority default: settings["url"] = d.WebhookURL } -- 2.52.0 From b7b8aa6f03678587dd0fb804fd1b25763a96872a Mon Sep 17 00:00:00 2001 From: Tyler Koenig Date: Fri, 15 May 2026 11:26:21 -0400 Subject: [PATCH 2/2] feat(metrics): add Prometheus /metrics endpoint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zero-dependency Prometheus text exposition format. Exposes monitor up/down, latency, status code, check timestamps, pause state, SSL cert expiry, and check counters — all from in-memory state. --- internal/metrics/prometheus.go | 99 +++++++++++++++++++++++++++++ internal/metrics/prometheus_test.go | 96 ++++++++++++++++++++++++++++ internal/server/server.go | 6 +- 3 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 internal/metrics/prometheus.go create mode 100644 internal/metrics/prometheus_test.go diff --git a/internal/metrics/prometheus.go b/internal/metrics/prometheus.go new file mode 100644 index 0000000..24f4faa --- /dev/null +++ b/internal/metrics/prometheus.go @@ -0,0 +1,99 @@ +package metrics + +import ( + "fmt" + "go-upkeep/internal/models" + "go-upkeep/internal/monitor" + "net/http" + "sort" + "strings" +) + +func Handler(eng *monitor.Engine) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + sites := eng.GetAllSites() + sort.Slice(sites, func(i, j int) bool { return sites[i].ID < sites[j].ID }) + + var b strings.Builder + + writeHelp(&b, "upkeep_monitor_up", "gauge", "Whether the monitor is up (1) or down (0).") + for _, s := range sites { + val := 0 + if s.Status == "UP" { + val = 1 + } + writeGauge(&b, "upkeep_monitor_up", labels(s), float64(val)) + } + + writeHelp(&b, "upkeep_monitor_latency_seconds", "gauge", "Last check latency in seconds.") + for _, s := range sites { + writeGauge(&b, "upkeep_monitor_latency_seconds", labels(s), s.Latency.Seconds()) + } + + writeHelp(&b, "upkeep_monitor_status_code", "gauge", "HTTP response status code of the last check.") + for _, s := range sites { + if s.Type != "http" { + continue + } + writeGauge(&b, "upkeep_monitor_status_code", labels(s), float64(s.StatusCode)) + } + + writeHelp(&b, "upkeep_monitor_check_timestamp_seconds", "gauge", "Unix timestamp of the last check.") + for _, s := range sites { + if s.LastCheck.IsZero() { + continue + } + writeGauge(&b, "upkeep_monitor_check_timestamp_seconds", labels(s), float64(s.LastCheck.Unix())) + } + + writeHelp(&b, "upkeep_monitor_paused", "gauge", "Whether the monitor is paused (1) or active (0).") + for _, s := range sites { + val := 0 + if s.Paused { + val = 1 + } + writeGauge(&b, "upkeep_monitor_paused", labels(s), float64(val)) + } + + writeHelp(&b, "upkeep_monitor_cert_expiry_timestamp_seconds", "gauge", "Unix timestamp when the SSL certificate expires.") + for _, s := range sites { + if !s.HasSSL || s.CertExpiry.IsZero() { + continue + } + writeGauge(&b, "upkeep_monitor_cert_expiry_timestamp_seconds", labels(s), float64(s.CertExpiry.Unix())) + } + + writeHelp(&b, "upkeep_monitor_checks_total", "counter", "Total number of checks performed.") + writeHelp(&b, "upkeep_monitor_checks_up_total", "counter", "Total number of successful checks.") + for _, s := range sites { + h, ok := eng.GetHistory(s.ID) + if !ok { + continue + } + writeGauge(&b, "upkeep_monitor_checks_total", labels(s), float64(h.TotalChecks)) + writeGauge(&b, "upkeep_monitor_checks_up_total", labels(s), float64(h.UpChecks)) + } + + w.Header().Set("Content-Type", "text/plain; version=0.0.4; charset=utf-8") + w.Write([]byte(b.String())) + } +} + +func labels(s models.Site) string { + return fmt.Sprintf(`id="%d",name="%s",type="%s"`, s.ID, escapeLabelValue(s.Name), s.Type) +} + +func escapeLabelValue(s string) string { + s = strings.ReplaceAll(s, `\`, `\\`) + s = strings.ReplaceAll(s, `"`, `\"`) + s = strings.ReplaceAll(s, "\n", `\n`) + return s +} + +func writeHelp(b *strings.Builder, name, typ, help string) { + fmt.Fprintf(b, "# HELP %s %s\n# TYPE %s %s\n", name, help, name, typ) +} + +func writeGauge(b *strings.Builder, name, labels string, val float64) { + fmt.Fprintf(b, "%s{%s} %g\n", name, labels, val) +} diff --git a/internal/metrics/prometheus_test.go b/internal/metrics/prometheus_test.go new file mode 100644 index 0000000..7cbf680 --- /dev/null +++ b/internal/metrics/prometheus_test.go @@ -0,0 +1,96 @@ +package metrics + +import ( + "context" + "go-upkeep/internal/models" + "go-upkeep/internal/monitor" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" +) + +type mockStore struct { + sites []models.Site +} + +func (m *mockStore) Init() error { return nil } +func (m *mockStore) GetSites() ([]models.Site, error) { return m.sites, nil } +func (m *mockStore) AddSite(models.Site) error { return nil } +func (m *mockStore) UpdateSite(models.Site) error { return nil } +func (m *mockStore) UpdateSitePaused(int, bool) error { return nil } +func (m *mockStore) DeleteSite(int) error { return nil } +func (m *mockStore) GetAllAlerts() ([]models.AlertConfig, error) { return nil, nil } +func (m *mockStore) GetAlert(int) (models.AlertConfig, error) { return models.AlertConfig{}, nil } +func (m *mockStore) AddAlert(string, string, map[string]string) error { return nil } +func (m *mockStore) UpdateAlert(int, string, string, map[string]string) error { return nil } +func (m *mockStore) DeleteAlert(int) error { return nil } +func (m *mockStore) GetAllUsers() ([]models.User, error) { return nil, nil } +func (m *mockStore) AddUser(string, string, string) error { return nil } +func (m *mockStore) UpdateUser(int, string, string, string) error { return nil } +func (m *mockStore) DeleteUser(int) error { return nil } +func (m *mockStore) SaveCheck(int, int64, bool) error { return nil } +func (m *mockStore) LoadAllHistory(int) (map[int][]models.CheckRecord, error) { + return nil, nil +} +func (m *mockStore) ExportData() (models.Backup, error) { return models.Backup{}, nil } +func (m *mockStore) ImportData(models.Backup) error { return nil } + +func TestMetricsHandler(t *testing.T) { + ms := &mockStore{ + sites: []models.Site{ + {ID: 1, Name: "Example", URL: "https://example.com", Type: "http", Interval: 30}, + {ID: 2, Name: "DNS Check", Type: "dns", Interval: 60}, + }, + } + eng := monitor.NewEngine(ms) + ctx, cancel := context.WithCancel(context.Background()) + eng.Start(ctx) + time.Sleep(100 * time.Millisecond) + + rec := httptest.NewRecorder() + Handler(eng)(rec, httptest.NewRequest("GET", "/metrics", nil)) + cancel() + + if rec.Code != http.StatusOK { + t.Fatalf("expected 200, got %d", rec.Code) + } + + body := rec.Body.String() + + ct := rec.Header().Get("Content-Type") + if !strings.Contains(ct, "text/plain") { + t.Errorf("expected text/plain content type, got %q", ct) + } + + expected := []string{ + "# HELP upkeep_monitor_up", + "# TYPE upkeep_monitor_up gauge", + `upkeep_monitor_up{id="1",name="Example",type="http"}`, + `upkeep_monitor_up{id="2",name="DNS Check",type="dns"}`, + "# HELP upkeep_monitor_latency_seconds", + "# HELP upkeep_monitor_paused", + "# HELP upkeep_monitor_checks_total", + } + for _, s := range expected { + if !strings.Contains(body, s) { + t.Errorf("missing expected line: %s", s) + } + } +} + +func TestEscapeLabelValue(t *testing.T) { + cases := []struct{ in, want string }{ + {`simple`, `simple`}, + {`has "quotes"`, `has \"quotes\"`}, + {"has\nnewline", `has\nnewline`}, + {`back\slash`, `back\\slash`}, + } + for _, tc := range cases { + got := escapeLabelValue(tc.in) + if got != tc.want { + t.Errorf("escapeLabelValue(%q) = %q, want %q", tc.in, got, tc.want) + } + } +} diff --git a/internal/server/server.go b/internal/server/server.go index ac26bd2..fdf7f9b 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "go-upkeep/internal/importer" + "go-upkeep/internal/metrics" "go-upkeep/internal/models" "go-upkeep/internal/monitor" "go-upkeep/internal/store" @@ -242,7 +243,10 @@ func Start(cfg ServerConfig, s store.Store, eng *monitor.Engine) { w.Write([]byte(fmt.Sprintf("Imported %d monitors, %d alerts from Kuma v%s", len(backup.Sites), len(backup.Alerts), kb.Version))) }) - // 6. Status Page + // 6. Prometheus Metrics + mux.HandleFunc("/metrics", metrics.Handler(eng)) + + // 7. Status Page if cfg.EnableStatus { mux.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { renderStatusPage(w, cfg.Title, eng) }) mux.HandleFunc("/status/json", func(w http.ResponseWriter, r *http.Request) { -- 2.52.0