New MCP server that queries live Prometheus and Alertmanager HTTP APIs with 8 tools: list_alerts, get_alert, search_metrics, get_metric_metadata, query (PromQL), list_targets, list_silences, and create_silence. Extends the MCP core with ModeCustom and NewGenericServer for servers that don't require a database. Includes CLI with direct commands (alerts, query, targets, metrics), NixOS module, and comprehensive httptest-based tests. Bumps existing binaries to 0.2.1 due to shared internal/mcp change. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
176 lines
5.0 KiB
Go
176 lines
5.0 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestAlertmanagerClient_ListAlerts(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v2/alerts" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`[
|
|
{
|
|
"annotations": {"summary": "Target is down"},
|
|
"endsAt": "2024-01-01T01:00:00Z",
|
|
"fingerprint": "abc123",
|
|
"receivers": [{"name": "default"}],
|
|
"startsAt": "2024-01-01T00:00:00Z",
|
|
"status": {"inhibitedBy": [], "silencedBy": [], "state": "active"},
|
|
"updatedAt": "2024-01-01T00:00:00Z",
|
|
"generatorURL": "http://prometheus:9090/graph",
|
|
"labels": {"alertname": "TargetDown", "severity": "critical", "instance": "node1:9100"}
|
|
}
|
|
]`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := NewAlertmanagerClient(srv.URL)
|
|
alerts, err := client.ListAlerts(context.Background(), AlertFilters{})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(alerts) != 1 {
|
|
t.Fatalf("expected 1 alert, got %d", len(alerts))
|
|
}
|
|
if alerts[0].Fingerprint != "abc123" {
|
|
t.Errorf("expected fingerprint=abc123, got %s", alerts[0].Fingerprint)
|
|
}
|
|
if alerts[0].Labels["alertname"] != "TargetDown" {
|
|
t.Errorf("expected alertname=TargetDown, got %s", alerts[0].Labels["alertname"])
|
|
}
|
|
if alerts[0].Status.State != "active" {
|
|
t.Errorf("expected state=active, got %s", alerts[0].Status.State)
|
|
}
|
|
}
|
|
|
|
func TestAlertmanagerClient_ListAlertsWithFilters(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
q := r.URL.Query()
|
|
if q.Get("active") != "true" {
|
|
t.Errorf("expected active=true, got %s", q.Get("active"))
|
|
}
|
|
if q.Get("silenced") != "false" {
|
|
t.Errorf("expected silenced=false, got %s", q.Get("silenced"))
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`[]`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := NewAlertmanagerClient(srv.URL)
|
|
active := true
|
|
silenced := false
|
|
_, err := client.ListAlerts(context.Background(), AlertFilters{
|
|
Active: &active,
|
|
Silenced: &silenced,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestAlertmanagerClient_ListSilences(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/v2/silences" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`[
|
|
{
|
|
"id": "silence-1",
|
|
"matchers": [{"name": "alertname", "value": "TargetDown", "isRegex": false}],
|
|
"startsAt": "2024-01-01T00:00:00Z",
|
|
"endsAt": "2024-01-01T02:00:00Z",
|
|
"createdBy": "admin",
|
|
"comment": "Maintenance window",
|
|
"status": {"state": "active"}
|
|
}
|
|
]`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := NewAlertmanagerClient(srv.URL)
|
|
silences, err := client.ListSilences(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(silences) != 1 {
|
|
t.Fatalf("expected 1 silence, got %d", len(silences))
|
|
}
|
|
if silences[0].ID != "silence-1" {
|
|
t.Errorf("expected id=silence-1, got %s", silences[0].ID)
|
|
}
|
|
if silences[0].CreatedBy != "admin" {
|
|
t.Errorf("expected createdBy=admin, got %s", silences[0].CreatedBy)
|
|
}
|
|
}
|
|
|
|
func TestAlertmanagerClient_CreateSilence(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
t.Errorf("expected POST, got %s", r.Method)
|
|
}
|
|
if r.URL.Path != "/api/v2/silences" {
|
|
t.Errorf("unexpected path: %s", r.URL.Path)
|
|
}
|
|
if r.Header.Get("Content-Type") != "application/json" {
|
|
t.Errorf("expected Content-Type=application/json, got %s", r.Header.Get("Content-Type"))
|
|
}
|
|
|
|
var silence Silence
|
|
if err := json.NewDecoder(r.Body).Decode(&silence); err != nil {
|
|
t.Fatalf("failed to decode request body: %v", err)
|
|
}
|
|
if silence.CreatedBy != "admin" {
|
|
t.Errorf("expected createdBy=admin, got %s", silence.CreatedBy)
|
|
}
|
|
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"silenceID": "new-silence-id"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := NewAlertmanagerClient(srv.URL)
|
|
id, err := client.CreateSilence(context.Background(), Silence{
|
|
Matchers: []Matcher{
|
|
{Name: "alertname", Value: "TargetDown", IsRegex: false},
|
|
},
|
|
StartsAt: time.Now(),
|
|
EndsAt: time.Now().Add(2 * time.Hour),
|
|
CreatedBy: "admin",
|
|
Comment: "Test silence",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if id != "new-silence-id" {
|
|
t.Errorf("expected id=new-silence-id, got %s", id)
|
|
}
|
|
}
|
|
|
|
func TestAlertmanagerClient_HTTPError(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
_, _ = w.Write([]byte("internal error"))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
client := NewAlertmanagerClient(srv.URL)
|
|
_, err := client.ListAlerts(context.Background(), AlertFilters{})
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
}
|