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:
135
internal/monitoring/prometheus.go
Normal file
135
internal/monitoring/prometheus.go
Normal file
@@ -0,0 +1,135 @@
|
||||
package monitoring
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PrometheusClient is an HTTP client for the Prometheus API.
|
||||
type PrometheusClient struct {
|
||||
baseURL string
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
// NewPrometheusClient creates a new Prometheus API client.
|
||||
func NewPrometheusClient(baseURL string) *PrometheusClient {
|
||||
return &PrometheusClient{
|
||||
baseURL: strings.TrimRight(baseURL, "/"),
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Query executes an instant PromQL query. If ts is zero, the current time is used.
|
||||
func (c *PrometheusClient) Query(ctx context.Context, promql string, ts time.Time) (*PromQueryData, error) {
|
||||
params := url.Values{}
|
||||
params.Set("query", promql)
|
||||
if !ts.IsZero() {
|
||||
params.Set("time", fmt.Sprintf("%d", ts.Unix()))
|
||||
}
|
||||
|
||||
body, err := c.get(ctx, "/api/v1/query", params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query failed: %w", err)
|
||||
}
|
||||
|
||||
var data PromQueryData
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse query data: %w", err)
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// LabelValues returns all values for a given label name.
|
||||
func (c *PrometheusClient) LabelValues(ctx context.Context, label string) ([]string, error) {
|
||||
path := fmt.Sprintf("/api/v1/label/%s/values", url.PathEscape(label))
|
||||
body, err := c.get(ctx, path, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("label values failed: %w", err)
|
||||
}
|
||||
|
||||
var values []string
|
||||
if err := json.Unmarshal(body, &values); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse label values: %w", err)
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
// Metadata returns metadata for metrics. If metric is empty, returns metadata for all metrics.
|
||||
func (c *PrometheusClient) Metadata(ctx context.Context, metric string) (map[string][]PromMetadata, error) {
|
||||
params := url.Values{}
|
||||
if metric != "" {
|
||||
params.Set("metric", metric)
|
||||
}
|
||||
|
||||
body, err := c.get(ctx, "/api/v1/metadata", params)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("metadata failed: %w", err)
|
||||
}
|
||||
|
||||
var metadata map[string][]PromMetadata
|
||||
if err := json.Unmarshal(body, &metadata); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse metadata: %w", err)
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
// Targets returns the current scrape targets.
|
||||
func (c *PrometheusClient) Targets(ctx context.Context) (*PromTargetsData, error) {
|
||||
body, err := c.get(ctx, "/api/v1/targets", nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("targets failed: %w", err)
|
||||
}
|
||||
|
||||
var data PromTargetsData
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse targets data: %w", err)
|
||||
}
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
// get performs a GET request and returns the "data" field from the Prometheus response envelope.
|
||||
func (c *PrometheusClient) get(ctx context.Context, path string, params url.Values) (json.RawMessage, error) {
|
||||
u := c.baseURL + path
|
||||
if len(params) > 0 {
|
||||
u += "?" + params.Encode()
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close() //nolint:errcheck // cleanup on exit
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var promResp PromResponse
|
||||
if err := json.Unmarshal(body, &promResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if promResp.Status != "success" {
|
||||
return nil, fmt.Errorf("prometheus error (%s): %s", promResp.ErrorType, promResp.Error)
|
||||
}
|
||||
|
||||
return promResp.Data, nil
|
||||
}
|
||||
Reference in New Issue
Block a user