This repository has been archived on 2026-03-10. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
labmcp/internal/monitoring/prometheus.go
Torjus Håkestad 1755364bba 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>
2026-02-04 23:11:53 +01:00

136 lines
3.7 KiB
Go

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
}