feat: add lab-monitoring MCP server for Prometheus and Alertmanager
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>
This commit is contained in:
175
internal/monitoring/alertmanager_test.go
Normal file
175
internal/monitoring/alertmanager_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user