diff --git a/cmd/obs-mcp/main.go b/cmd/obs-mcp/main.go index 5dea0e41..721d13ee 100644 --- a/cmd/obs-mcp/main.go +++ b/cmd/obs-mcp/main.go @@ -5,13 +5,12 @@ import ( "flag" "fmt" "log" - "log/slog" "os" "slices" "strings" "github.com/modelcontextprotocol/go-sdk/mcp" - "github.com/prometheus/common/promslog" + "k8s.io/klog/v2" "github.com/rhobs/obs-mcp/pkg/auth" "github.com/rhobs/obs-mcp/pkg/k8s" @@ -66,8 +65,10 @@ func main() { os.Exit(0) } - // Configure slog with specified log level + // Configure klog with specified log level configureLogging(*logLevel) + ctx := context.Background() + logger := klog.FromContext(ctx) // Parse and validate auth mode parsedAuthMode, err := auth.ParseAuthMode(*authMode) @@ -92,7 +93,7 @@ func main() { metricsBackendURL := "" metricsURLSource := "" if slices.Contains(parsedToolsets, mcpserver.ToolsetMetrics) { - metricsBackendURL, metricsURLSource, err = determineMetricsBackendURL(parsedAuthMode, parsedMetricsBackend) + metricsBackendURL, metricsURLSource, err = determineMetricsBackendURL(ctx, parsedAuthMode, parsedMetricsBackend) if err != nil { log.Fatalf("%v", err) } @@ -101,7 +102,7 @@ func main() { alertmanagerURL := "" alertmanagerURLSource := "" if slices.Contains(parsedToolsets, mcpserver.ToolsetMetrics) { - alertmanagerURL, alertmanagerURLSource, err = determineAlertmanagerURL(parsedAuthMode) + alertmanagerURL, alertmanagerURLSource, err = determineAlertmanagerURL(ctx, parsedAuthMode) if err != nil { log.Fatalf("%v", err) } @@ -111,7 +112,7 @@ func main() { lokiResolvedURL := "" lokiURLSource := "" if slices.Contains(parsedToolsets, mcpserver.ToolsetLogs) { - lokiResolvedURL, lokiURLSource, err = determineLokiURL(parsedAuthMode, *lokiURL, *lokiUseRoute) + lokiResolvedURL, lokiURLSource, err = determineLokiURL(ctx, parsedAuthMode, *lokiURL, *lokiUseRoute) if err != nil { log.Fatalf("%v", err) } @@ -155,7 +156,7 @@ func main() { tempoResolvedURL := "" tempoURLSource := "" if slices.Contains(parsedToolsets, mcpserver.ToolsetTraces) { - tempoResolvedURL, tempoURLSource = determineTempoURL(*tempoURL) + tempoResolvedURL, tempoURLSource = determineTempoURL(ctx, *tempoURL) } // Create MCP options @@ -184,7 +185,7 @@ func main() { log.Fatalf("Failed to create MCP server: %v", err) } - slog.Info("Starting server", + logger.Info("Starting server", "toolsets", opts.Toolsets, "auth_mode", opts.AuthMode, "metrics_backend_url", opts.MetricsBackendURL, @@ -201,14 +202,13 @@ func main() { // Choose server mode based on flags if *listen != "" { // HTTP mode - ctx := context.Background() if err := mcpserver.Serve(ctx, mcpServer, *listen, opts.AuthMode); err != nil { log.Fatalf("HTTP server failed: %v", err) } } else { // Start server on stdio (default mode) transport := &mcp.StdioTransport{} - if _, err := mcpServer.Connect(context.Background(), transport, nil); err != nil { + if _, err := mcpServer.Connect(ctx, transport, nil); err != nil { log.Fatalf("Server failed: %v", err) } } @@ -241,16 +241,17 @@ func parseMetricsBackend(backend string) (k8s.MetricsBackend, error) { // determineMetricsBackendURL determines the metrics backend URL based on auth mode and environment. // Returns the resolved URL, a source description for logging, and an error if the configuration is invalid. -func determineMetricsBackendURL(authMode auth.AuthMode, backend k8s.MetricsBackend) (url, source string, err error) { +func determineMetricsBackendURL(ctx context.Context, authMode auth.AuthMode, backend k8s.MetricsBackend) (url, source string, err error) { + logger := klog.FromContext(ctx) if prometheusURL := os.Getenv("PROMETHEUS_URL"); prometheusURL != "" { return prometheusURL, "PROMETHEUS_URL env var", nil } if authMode == auth.AuthModeKubeConfig { - slog.Info("No PROMETHEUS_URL set, attempting route discovery", "backend", backend) - url, err := k8s.GetMetricsBackendURL(backend) + logger.Info("No PROMETHEUS_URL set, attempting route discovery", "backend", string(backend)) + url, err := k8s.GetMetricsBackendURL(ctx, backend) if err != nil { - slog.Warn("Route discovery failed, falling back to default", "err", err, "default", defaultPrometheusURL) + logger.Info("Route discovery failed, falling back to default", "err", err, "default", defaultPrometheusURL) return defaultPrometheusURL, "default (route discovery failed)", nil } return url, "route discovery", nil @@ -267,16 +268,17 @@ func determineMetricsBackendURL(authMode auth.AuthMode, backend k8s.MetricsBacke // determineAlertmanagerURL determines the Alertmanager URL based on auth mode and environment. // Returns the resolved URL, a source description for logging, and an error if the configuration is invalid. -func determineAlertmanagerURL(authMode auth.AuthMode) (url, source string, err error) { +func determineAlertmanagerURL(ctx context.Context, authMode auth.AuthMode) (url, source string, err error) { + logger := klog.FromContext(ctx) if alertmanagerURL := os.Getenv("ALERTMANAGER_URL"); alertmanagerURL != "" { return alertmanagerURL, "ALERTMANAGER_URL env var", nil } if authMode == auth.AuthModeKubeConfig { - slog.Info("No ALERTMANAGER_URL set, attempting route discovery") - url, err := k8s.GetAlertmanagerURL() + logger.Info("No ALERTMANAGER_URL set, attempting route discovery") + url, err := k8s.GetAlertmanagerURL(ctx) if err != nil { - slog.Warn("Route discovery failed, falling back to default", "err", err, "default", defaultAlertmanagerURL) + logger.Info("Route discovery failed, falling back to default", "err", err, "default", defaultAlertmanagerURL) return defaultAlertmanagerURL, "default (route discovery failed)", nil } return url, "route discovery", nil @@ -289,18 +291,20 @@ func determineAlertmanagerURL(authMode auth.AuthMode) (url, source string, err e ) } -func determineTempoURL(flagURL string) (url, source string) { +func determineTempoURL(ctx context.Context, flagURL string) (url, source string) { + logger := klog.FromContext(ctx) if flagURL != "" { return flagURL, "--traces.tempo-url flag" } if tempoURL := os.Getenv("TEMPO_URL"); tempoURL != "" { return tempoURL, "TEMPO_URL env var" } - slog.Info("No Tempo URL configured; Tempo tools require tempoNamespace+tempoName discovery parameters or explicit Tempo URL") + logger.Info("No Tempo URL configured; Tempo tools require tempoNamespace+tempoName discovery parameters or explicit Tempo URL") return "", "unset" } -func determineLokiURL(authMode auth.AuthMode, flagURL string, useRoute bool) (url, source string, err error) { +func determineLokiURL(ctx context.Context, authMode auth.AuthMode, flagURL string, useRoute bool) (url, source string, err error) { + logger := klog.FromContext(ctx) if flagURL != "" { return flagURL, "--loki-url flag", nil } @@ -308,10 +312,10 @@ func determineLokiURL(authMode auth.AuthMode, flagURL string, useRoute bool) (ur return lokiURL, "LOKI_URL env var", nil } if authMode == auth.AuthModeKubeConfig && !useRoute { - slog.Warn("No Loki URL configured, falling back to default", "default", defaultLokiURL) + logger.Info("No Loki URL configured, falling back to default", "default", defaultLokiURL) return defaultLokiURL, "default", nil } - slog.Warn("No Loki URL configured; Loki tools require lokiNamespace+lokiName discovery parameters or explicit Loki URL") + logger.Info("No Loki URL configured; Loki tools require lokiNamespace+lokiName discovery parameters or explicit Loki URL") return "", "unset", nil } @@ -327,24 +331,22 @@ func isFlagExplicitlySet(name string) bool { return found } -// configureLogging sets up the slog logger with the specified log level +// configureLogging sets up klog with the specified log level func configureLogging(levelStr string) { - level := promslog.NewLevel() - err := level.Set(levelStr) - if err != nil { - log.Fatal(err.Error()) - } + fs := flag.NewFlagSet("klog", flag.ContinueOnError) + klog.InitFlags(fs) - format := promslog.NewFormat() - err = format.Set("logfmt") - if err != nil { - log.Fatal(err.Error()) + var verbosity string + switch strings.ToLower(levelStr) { + case "debug": + verbosity = "4" + default: + verbosity = "0" + } + if err := fs.Set("v", verbosity); err != nil { + log.Fatalf("Failed to set klog verbosity: %v", err) + } + if err := fs.Set("logtostderr", "true"); err != nil { + log.Fatalf("Failed to set klog logtostderr: %v", err) } - - logger := promslog.New(&promslog.Config{ - Level: level, - Format: format, - Style: promslog.GoKitStyle, - }) - slog.SetDefault(logger) } diff --git a/cmd/obs-mcp/main_test.go b/cmd/obs-mcp/main_test.go index f0455949..353fd8f2 100644 --- a/cmd/obs-mcp/main_test.go +++ b/cmd/obs-mcp/main_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "testing" "github.com/rhobs/obs-mcp/pkg/auth" @@ -108,7 +109,7 @@ func TestDetermineMetricsBackendURL_RequiresURLForNonKubeconfigModes(t *testing. for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - _, _, err := determineMetricsBackendURL(tt.authMode, tt.backend) + _, _, err := determineMetricsBackendURL(context.Background(), tt.authMode, tt.backend) if err == nil { t.Errorf("expected error for auth mode %q without PROMETHEUS_URL, got nil", tt.authMode) } @@ -131,7 +132,7 @@ func TestDetermineMetricsBackendURL_EnvVarOverridesAll(t *testing.T) { for _, authMode := range authModes { t.Run(string(authMode), func(t *testing.T) { - url, source, err := determineMetricsBackendURL(authMode, k8s.MetricsBackendThanos) + url, source, err := determineMetricsBackendURL(context.Background(), authMode, k8s.MetricsBackendThanos) if err != nil { t.Errorf("unexpected error: %v", err) } @@ -148,7 +149,7 @@ func TestDetermineMetricsBackendURL_EnvVarOverridesAll(t *testing.T) { func TestDetermineLokiURL(t *testing.T) { t.Run("explicit flag wins", func(t *testing.T) { t.Setenv("LOKI_URL", "http://from-env:3100") - got, source, err := determineLokiURL(auth.AuthModeHeader, "http://from-flag:3100", false) + got, source, err := determineLokiURL(context.Background(), auth.AuthModeHeader, "http://from-flag:3100", false) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -159,7 +160,7 @@ func TestDetermineLokiURL(t *testing.T) { t.Run("env used when flag missing", func(t *testing.T) { t.Setenv("LOKI_URL", "http://from-env:3100") - got, source, err := determineLokiURL(auth.AuthModeHeader, "", false) + got, source, err := determineLokiURL(context.Background(), auth.AuthModeHeader, "", false) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -170,7 +171,7 @@ func TestDetermineLokiURL(t *testing.T) { t.Run("kubeconfig falls back to default", func(t *testing.T) { t.Setenv("LOKI_URL", "") - got, source, err := determineLokiURL(auth.AuthModeKubeConfig, "", false) + got, source, err := determineLokiURL(context.Background(), auth.AuthModeKubeConfig, "", false) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -181,7 +182,7 @@ func TestDetermineLokiURL(t *testing.T) { t.Run("non-kubeconfig without URL returns unset", func(t *testing.T) { t.Setenv("LOKI_URL", "") - got, source, err := determineLokiURL(auth.AuthModeHeader, "", false) + got, source, err := determineLokiURL(context.Background(), auth.AuthModeHeader, "", false) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -194,7 +195,7 @@ func TestDetermineLokiURL(t *testing.T) { func TestDetermineTempoURL(t *testing.T) { t.Run("explicit flag wins", func(t *testing.T) { t.Setenv("TEMPO_URL", "http://from-env:3200") - got, source := determineTempoURL("http://from-flag:3200") + got, source := determineTempoURL(context.Background(), "http://from-flag:3200") if got != "http://from-flag:3200" || source != "--traces.tempo-url flag" { t.Fatalf("unexpected result: %s (%s)", got, source) } @@ -202,7 +203,7 @@ func TestDetermineTempoURL(t *testing.T) { t.Run("env used when flag missing", func(t *testing.T) { t.Setenv("TEMPO_URL", "http://from-env:3200") - got, source := determineTempoURL("") + got, source := determineTempoURL(context.Background(), "") if got != "http://from-env:3200" || source != "TEMPO_URL env var" { t.Fatalf("unexpected result: %s (%s)", got, source) } @@ -210,7 +211,7 @@ func TestDetermineTempoURL(t *testing.T) { t.Run("no URL returns unset", func(t *testing.T) { t.Setenv("TEMPO_URL", "") - got, source := determineTempoURL("") + got, source := determineTempoURL(context.Background(), "") if got != "" || source != "unset" { t.Fatalf("unexpected result: %s (%s)", got, source) } diff --git a/go.mod b/go.mod index 5c79dba1..80619746 100644 --- a/go.mod +++ b/go.mod @@ -116,7 +116,7 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect k8s.io/cli-runtime v0.36.2 // indirect - k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/klog/v2 v2.140.0 k8s.io/kube-openapi v0.0.0-20260603220949-865597e52e25 // indirect k8s.io/kubectl v0.36.2 // indirect k8s.io/metrics v0.36.2 // indirect diff --git a/pkg/alertmanager/loader.go b/pkg/alertmanager/loader.go index 98baabc6..013e7e28 100644 --- a/pkg/alertmanager/loader.go +++ b/pkg/alertmanager/loader.go @@ -3,12 +3,13 @@ package alertmanager import ( "context" "fmt" - "log/slog" "net/http" "net/url" "strings" "time" + "k8s.io/klog/v2" + httptransport "github.com/go-openapi/runtime/client" "github.com/prometheus/alertmanager/api/v2/client" "github.com/prometheus/alertmanager/api/v2/client/alert" @@ -90,11 +91,11 @@ func (a *RealLoader) GetAlerts(ctx context.Context, active, silenced, inhibited, resp, err := a.client.Alert.GetAlerts(params) duration := time.Since(start) if err != nil { - slog.Error("Backend call failed", "backend", "alertmanager", "operation", "alerts", - "duration_ms", duration.Milliseconds(), "error", err) + klog.FromContext(ctx).Error(err, "Backend call failed", "backend", "alertmanager", "operation", "alerts", + "duration_ms", duration.Milliseconds()) return nil, fmt.Errorf("error fetching alerts: %w", err) } - slog.Debug("Backend call completed", "backend", "alertmanager", "operation", "alerts", + klog.FromContext(ctx).V(4).Info("Backend call completed", "backend", "alertmanager", "operation", "alerts", "duration_ms", duration.Milliseconds(), "result_count", len(resp.Payload)) return resp.Payload, nil @@ -111,11 +112,11 @@ func (a *RealLoader) GetSilences(ctx context.Context, filter []string) (models.G resp, err := a.client.Silence.GetSilences(params) duration := time.Since(start) if err != nil { - slog.Error("Backend call failed", "backend", "alertmanager", "operation", "silences", - "duration_ms", duration.Milliseconds(), "error", err) + klog.FromContext(ctx).Error(err, "Backend call failed", "backend", "alertmanager", "operation", "silences", + "duration_ms", duration.Milliseconds()) return nil, fmt.Errorf("error fetching silences: %w", err) } - slog.Debug("Backend call completed", "backend", "alertmanager", "operation", "silences", + klog.FromContext(ctx).V(4).Info("Backend call completed", "backend", "alertmanager", "operation", "silences", "duration_ms", duration.Milliseconds(), "result_count", len(resp.Payload)) return resp.Payload, nil diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index cb422d0c..4ddff537 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -5,11 +5,12 @@ import ( "crypto/tls" "crypto/x509" "fmt" - "log/slog" "net/http" "os" "strings" + "k8s.io/klog/v2" + "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" promapi "github.com/prometheus/client_golang/api" promcfg "github.com/prometheus/common/config" @@ -62,10 +63,10 @@ func BuildRoundTripper(ctx context.Context, restConfig *rest.Config, authMode Au return nil, err } - return createRoundTripperWithToken(restConfig, token, useTLS, insecure) + return createRoundTripperWithToken(ctx, restConfig, token, useTLS, insecure) } -func createRoundTripperWithToken(restConfig *rest.Config, token string, useTLS, insecure bool) (http.RoundTripper, error) { +func createRoundTripperWithToken(ctx context.Context, restConfig *rest.Config, token string, useTLS, insecure bool) (http.RoundTripper, error) { defaultRt, ok := promapi.DefaultRoundTripper.(*http.Transport) if !ok { return nil, fmt.Errorf("unexpected RoundTripper type: %T, expected *http.Transport", promapi.DefaultRoundTripper) @@ -73,7 +74,7 @@ func createRoundTripperWithToken(restConfig *rest.Config, token string, useTLS, rt := defaultRt.Clone() if !useTLS { - slog.Warn("Connecting without TLS") + klog.FromContext(ctx).Info("Connecting without TLS") return rt, nil } @@ -83,7 +84,7 @@ func createRoundTripperWithToken(restConfig *rest.Config, token string, useTLS, InsecureSkipVerify: true, } } else { - certs, err := createCertPoolFromRESTConfig(restConfig) + certs, err := createCertPoolFromRESTConfig(ctx, restConfig) if err != nil { return nil, err } @@ -102,7 +103,7 @@ func createRoundTripperWithToken(restConfig *rest.Config, token string, useTLS, } // createCertPoolFromRESTConfig creates a cert pool from Kubernetes REST config. -func createCertPoolFromRESTConfig(restConfig *rest.Config) (*x509.CertPool, error) { +func createCertPoolFromRESTConfig(ctx context.Context, restConfig *rest.Config) (*x509.CertPool, error) { var certPool *x509.CertPool // Start with system cert pool if available @@ -119,9 +120,9 @@ func createCertPoolFromRESTConfig(restConfig *rest.Config) (*x509.CertPool, erro if len(restConfig.CAData) > 0 { if ok := certPool.AppendCertsFromPEM(restConfig.CAData); ok { caLoaded = true - slog.Debug("Loaded cluster CA from REST config CAData") + klog.FromContext(ctx).V(4).Info("Loaded cluster CA from REST config CAData") } else { - slog.Warn("Failed to parse CA certificates from REST config CAData") + klog.FromContext(ctx).Info("Failed to parse CA certificates from REST config CAData") } } @@ -129,12 +130,12 @@ func createCertPoolFromRESTConfig(restConfig *rest.Config) (*x509.CertPool, erro if !caLoaded { caPEM, err := os.ReadFile(serviceCAFile) if err != nil { - slog.Warn("Failed to read CA file", "file", serviceCAFile, "error", err) + klog.FromContext(ctx).Info("Failed to read CA file", "file", serviceCAFile, "error", err) } else { if ok := certPool.AppendCertsFromPEM(caPEM); ok { - slog.Debug("Loaded cluster CA from file", "file", serviceCAFile) + klog.FromContext(ctx).V(4).Info("Loaded cluster CA from file", "file", serviceCAFile) } else { - slog.Warn("Failed to parse CA certificates from file", "file", serviceCAFile) + klog.FromContext(ctx).Info("Failed to parse CA certificates from file", "file", serviceCAFile) } } } diff --git a/pkg/auth/token.go b/pkg/auth/token.go index c5d605cb..eac53df4 100644 --- a/pkg/auth/token.go +++ b/pkg/auth/token.go @@ -3,10 +3,11 @@ package auth import ( "context" "fmt" - "log/slog" "os" "strings" + "k8s.io/klog/v2" + "github.com/containers/kubernetes-mcp-server/pkg/kubernetes" "k8s.io/client-go/rest" ) @@ -32,7 +33,7 @@ func readToken(ctx context.Context, restConfig *rest.Config, authMode AuthMode) // The caller is responsible for putting the token from the request header into the context. token := readTokenFromContext(ctx) if token == "" { - slog.Warn("no bearer token found in request context authorization header") + klog.FromContext(ctx).Info("no bearer token found in request context authorization header") } return token, nil diff --git a/pkg/k8s/client.go b/pkg/k8s/client.go index 75497441..a7090105 100644 --- a/pkg/k8s/client.go +++ b/pkg/k8s/client.go @@ -4,12 +4,12 @@ import ( "context" "encoding/json" "fmt" - "log/slog" "time" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" + "k8s.io/klog/v2" ) const ( @@ -68,33 +68,37 @@ func GetKubeClient() (*kubernetes.Clientset, error) { } // GetMetricsBackendURL discovers the metrics backend endpoint in OpenShift. -func GetMetricsBackendURL(backend MetricsBackend) (string, error) { +func GetMetricsBackendURL(ctx context.Context, backend MetricsBackend) (string, error) { + logger := klog.FromContext(ctx) if backend == MetricsBackendPrometheus { - return discoverRoute(prometheusRouteName) + return discoverRoute(ctx, prometheusRouteName) } - // Thanos with fallback to Prometheus - url, err := discoverRoute(thanosQuerierRouteName) + // Thanos with fallback to Prometheus — use getRouteURL directly to + // avoid error-level logging for the expected "not found" case. + url, err := getRouteURL(ctx, thanosQuerierRouteName) if err == nil { + logger.Info("Successfully discovered route", "route", thanosQuerierRouteName, "url", url) return url, nil } - slog.Info("Thanos route not found, falling back to prometheus", "error", err) - return discoverRoute(prometheusRouteName) + logger.Info("Thanos route not found, falling back to prometheus", "error", err) + return discoverRoute(ctx, prometheusRouteName) } // discoverRoute attempts to find a route and logs the result. -func discoverRoute(routeName string) (string, error) { - url, err := getRouteURL(routeName) +func discoverRoute(ctx context.Context, routeName string) (string, error) { + logger := klog.FromContext(ctx) + url, err := getRouteURL(ctx, routeName) if err != nil { - slog.Error("Failed to discover route", "route", routeName, "error", err) + logger.Error(err, "Failed to discover route", "route", routeName) return "", err } - slog.Info("Successfully discovered route", "route", routeName, "url", url) + logger.Info("Successfully discovered route", "route", routeName, "url", url) return url, nil } -func getRouteURL(routeName string) (string, error) { - ctx, cancel := context.WithTimeout(context.Background(), routeDiscoveryTimeout) +func getRouteURL(ctx context.Context, routeName string) (string, error) { + ctx, cancel := context.WithTimeout(ctx, routeDiscoveryTimeout) defer cancel() kubeClient, err := GetKubeClient() @@ -141,6 +145,6 @@ func parseHostFromRouteBody(body []byte) (string, error) { } // GetAlertmanagerURL discovers the Alertmanager endpoint in OpenShift. -func GetAlertmanagerURL() (string, error) { - return discoverRoute(alertmanagerRouteName) +func GetAlertmanagerURL(ctx context.Context) (string, error) { + return discoverRoute(ctx, alertmanagerRouteName) } diff --git a/pkg/logs/loki/loader.go b/pkg/logs/loki/loader.go index cea0374c..921a5e5f 100644 --- a/pkg/logs/loki/loader.go +++ b/pkg/logs/loki/loader.go @@ -5,13 +5,14 @@ import ( "encoding/json" "fmt" "io" - "log/slog" "net/http" "net/url" "strconv" "strings" "time" + "k8s.io/klog/v2" + promapi "github.com/prometheus/client_golang/api" ) @@ -218,7 +219,7 @@ func (l *RealLoader) getJSON(ctx context.Context, endpoint string, params url.Va } defer func() { _ = resp.Body.Close() }() - slog.Debug("Backend call completed", + klog.FromContext(ctx).V(4).Info("Backend call completed", "backend", "loki", "endpoint", endpoint, "status_code", resp.StatusCode, diff --git a/pkg/mcp/server.go b/pkg/mcp/server.go index 8c81122b..986dde5a 100644 --- a/pkg/mcp/server.go +++ b/pkg/mcp/server.go @@ -3,7 +3,6 @@ package mcp import ( "context" "errors" - "log/slog" "net/http" "os" "os/signal" @@ -14,6 +13,7 @@ import ( "github.com/modelcontextprotocol/go-sdk/mcp" "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" "github.com/rhobs/obs-mcp/pkg/auth" "github.com/rhobs/obs-mcp/pkg/k8s" @@ -150,10 +150,10 @@ func SetupTools(mcpServer *mcp.Server, opts ObsMCPOptions) error { if c, err := dynamic.NewForConfig(restConfig); err == nil { logsDynamicClient = c } else { - slog.Warn("LokiStack discovery disabled: failed to create Kubernetes dynamic client", "err", err) + klog.Background().Info("LokiStack discovery disabled: failed to create Kubernetes dynamic client", "err", err) } } else { - slog.Warn("LokiStack discovery disabled: failed to get Kubernetes client config", "err", err) + klog.Background().Info("LokiStack discovery disabled: failed to get Kubernetes client config", "err", err) } newLokiClient := func(ctx context.Context, url, tenant string) (lokiclient.Loader, error) { @@ -175,18 +175,36 @@ func authMiddleware(next http.Handler) http.Handler { }) } +var sensitiveHeaders = map[string]bool{ + "Authorization": true, + "Cookie": true, + "Set-Cookie": true, +} + +func redactHeaders(h http.Header) http.Header { + redacted := h.Clone() + for key := range redacted { + if sensitiveHeaders[key] { + redacted.Set(key, "[REDACTED]") + } + } + return redacted +} + func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - slog.Info("Incoming request", "method", r.Method, "path", r.URL.Path, "remote_addr", r.RemoteAddr) - slog.Debug("Request headers", "headers", r.Header) + logger := klog.FromContext(r.Context()) + logger.Info("Incoming request", "method", r.Method, "path", r.URL.Path, "remote_addr", r.RemoteAddr) + logger.V(4).Info("Request headers", "headers", redactHeaders(r.Header)) if r.ContentLength > 0 { - slog.Info("Request content length", "content_length", r.ContentLength) + logger.Info("Request content length", "content_length", r.ContentLength) } next.ServeHTTP(w, r) }) } func Serve(ctx context.Context, mcpServer *mcp.Server, listenAddr string, authMode auth.AuthMode) error { + logger := klog.FromContext(ctx) mux := http.NewServeMux() handler := loggingMiddleware(mux) @@ -222,7 +240,7 @@ func Serve(ctx context.Context, mcpServer *mcp.Server, listenAddr string, authMo serverErr := make(chan error, 1) go func() { - slog.Info("HTTP server starting", "listen_addr", listenAddr, "mcp_endpoint", mcpEndpoint) + logger.Info("HTTP server starting", "listen_addr", listenAddr, "mcp_endpoint", mcpEndpoint) if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { serverErr <- err } @@ -230,24 +248,24 @@ func Serve(ctx context.Context, mcpServer *mcp.Server, listenAddr string, authMo select { case sig := <-sigChan: - slog.Warn("Received signal, initiating graceful shutdown", "signal", sig) + logger.Info("Received signal, initiating graceful shutdown", "signal", sig) cancel() case <-ctx.Done(): - slog.Warn("Context cancelled, initiating graceful shutdown") + logger.Info("Context cancelled, initiating graceful shutdown") case err := <-serverErr: - slog.Error("HTTP server error", "error", err) + logger.Error(err, "HTTP server error") return err } shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), defaultShutdownTimeout) defer shutdownCancel() - slog.Info("Shutting down HTTP server gracefully") + logger.Info("Shutting down HTTP server gracefully") if err := httpServer.Shutdown(shutdownCtx); err != nil { - slog.Error("HTTP server shutdown error", "error", err) + logger.Error(err, "HTTP server shutdown error") return err } - slog.Info("HTTP server shutdown complete") + logger.Info("HTTP server shutdown complete") return nil } diff --git a/pkg/otelcol/handlers.go b/pkg/otelcol/handlers.go index 54f95ee5..303263d6 100644 --- a/pkg/otelcol/handlers.go +++ b/pkg/otelcol/handlers.go @@ -4,9 +4,10 @@ import ( "context" "fmt" "io/fs" - "log/slog" "strings" + "k8s.io/klog/v2" + "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/pavolloffay/opentelemetry-mcp-server/modules/collectorschema" @@ -178,8 +179,8 @@ func getSchemaLoaderFromConfig(config *Config) (SchemaLoader, error) { // ListComponentsHandler handles the listing of available components. func ListComponentsHandler(ctx context.Context, loader SchemaLoader, input ListComponentsInput) *resultutil.Result { - slog.Info("ListComponentsHandler called") - slog.Debug("ListComponentsHandler params", "input", input) + klog.FromContext(ctx).Info("ListComponentsHandler called") + klog.FromContext(ctx).V(4).Info("ListComponentsHandler params", "input", input) version := normalizeVersion(input.Version) if version == "" { @@ -209,7 +210,7 @@ func ListComponentsHandler(ctx context.Context, loader SchemaLoader, input ListC output.Components[string(k)] = v } - slog.Info("ListComponentsHandler executed successfully", + klog.FromContext(ctx).Info("ListComponentsHandler executed successfully", "receivers", len(output.Receivers), "processors", len(output.Processors), "exporters", len(output.Exporters)) @@ -219,8 +220,8 @@ func ListComponentsHandler(ctx context.Context, loader SchemaLoader, input ListC // GetComponentSchemaHandler handles getting a component's schema. func GetComponentSchemaHandler(ctx context.Context, loader SchemaLoader, input GetComponentSchemaInput) *resultutil.Result { - slog.Info("GetComponentSchemaHandler called") - slog.Debug("GetComponentSchemaHandler params", "input", input) + klog.FromContext(ctx).Info("GetComponentSchemaHandler called") + klog.FromContext(ctx).V(4).Info("GetComponentSchemaHandler params", "input", input) if !input.ComponentType.IsValid() { return resultutil.NewErrorResult(fmt.Errorf("invalid component_type: %s, must be one of: receiver, processor, exporter, extension, connector", input.ComponentType)) @@ -251,14 +252,14 @@ func GetComponentSchemaHandler(ctx context.Context, loader SchemaLoader, input G Schema: schema.Schema, } - slog.Info("GetComponentSchemaHandler executed successfully", "component", input.ComponentName) + klog.FromContext(ctx).Info("GetComponentSchemaHandler executed successfully", "component", input.ComponentName) return resultutil.NewSuccessResult(output) } // ValidateConfigHandler handles validating a component configuration. func ValidateConfigHandler(ctx context.Context, loader SchemaLoader, input ValidateConfigInput) *resultutil.Result { - slog.Info("ValidateConfigHandler called") - slog.Debug("ValidateConfigHandler params", "componentType", input.ComponentType, "componentName", input.ComponentName) + klog.FromContext(ctx).Info("ValidateConfigHandler called") + klog.FromContext(ctx).V(4).Info("ValidateConfigHandler params", "componentType", input.ComponentType, "componentName", input.ComponentName) if !input.ComponentType.IsValid() { return resultutil.NewErrorResult(fmt.Errorf("invalid component_type: %s, must be one of: receiver, processor, exporter, extension, connector", input.ComponentType)) @@ -309,13 +310,13 @@ func ValidateConfigHandler(ctx context.Context, loader SchemaLoader, input Valid Version: version, } - slog.Info("ValidateConfigHandler executed successfully", "valid", output.Valid, "errorCount", len(output.Errors)) + klog.FromContext(ctx).Info("ValidateConfigHandler executed successfully", "valid", output.Valid, "errorCount", len(output.Errors)) return resultutil.NewSuccessResult(output) } // GetVersionsHandler handles listing available versions. func GetVersionsHandler(ctx context.Context, loader SchemaLoader, input GetVersionsInput) *resultutil.Result { - slog.Info("GetVersionsHandler called") + klog.FromContext(ctx).Info("GetVersionsHandler called") versions, err := loader.GetAllVersions() if err != nil { @@ -332,6 +333,6 @@ func GetVersionsHandler(ctx context.Context, loader SchemaLoader, input GetVersi LatestVersion: latestVersion, } - slog.Info("GetVersionsHandler executed successfully", "versionCount", len(output.Versions)) + klog.FromContext(ctx).Info("GetVersionsHandler executed successfully", "versionCount", len(output.Versions)) return resultutil.NewSuccessResult(output) } diff --git a/pkg/prometheus/loader.go b/pkg/prometheus/loader.go index ef3e4027..f031ee2b 100644 --- a/pkg/prometheus/loader.go +++ b/pkg/prometheus/loader.go @@ -4,11 +4,12 @@ import ( "context" "errors" "fmt" - "log/slog" "regexp" "strings" "time" + "k8s.io/klog/v2" + "github.com/prometheus/client_golang/api" v1 "github.com/prometheus/client_golang/api/prometheus/v1" ) @@ -83,11 +84,11 @@ func (p *RealLoader) ListMetrics(ctx context.Context, nameRegex string) ([]strin labelValues, _, err := p.client.LabelValues(ctx, "__name__", matches, time.Now().Add(-ListMetricsTimeRange), time.Now()) duration := time.Since(start) if err != nil { - slog.Error("Backend call failed", "backend", p.backend, "operation", "list_metrics", - "duration_ms", duration.Milliseconds(), "error", err) + klog.FromContext(ctx).Error(err, "Backend call failed", "backend", p.backend, "operation", "list_metrics", + "duration_ms", duration.Milliseconds()) return nil, fmt.Errorf("error fetching metric names: %w", err) } - slog.Debug("Backend call completed", "backend", p.backend, "operation", "list_metrics", + klog.FromContext(ctx).V(4).Info("Backend call completed", "backend", p.backend, "operation", "list_metrics", "duration_ms", duration.Milliseconds(), "result_count", len(labelValues)) metrics := make([]string, len(labelValues)) @@ -135,7 +136,7 @@ func (p *RealLoader) ValidateMetricsExist(ctx context.Context, query string) err // the query passes any configured guardrails. func (p *RealLoader) validateQuery(ctx context.Context, query string) error { if err := p.ValidateMetricsExist(ctx, query); err != nil { - slog.Warn("Query validation rejected", "reason", "metric-not-found", "query", query, "error", err) + klog.FromContext(ctx).Info("Query validation rejected", "reason", "metric-not-found", "query", query, "error", err) return fmt.Errorf("metric validation failed: %w", err) } @@ -147,7 +148,7 @@ func (p *RealLoader) validateQuery(ctx context.Context, query string) error { if errors.As(err, &gv) { guardrail = gv.Guardrail } - slog.Warn("Guardrail rejected query", "guardrail", guardrail, "query", query, "error", err) + klog.FromContext(ctx).Info("Guardrail rejected query", "guardrail", guardrail, "query", query, "error", err) return fmt.Errorf("query validation failed: %w", err) } if !isSafe { @@ -173,11 +174,11 @@ func (p *RealLoader) ExecuteRangeQuery(ctx context.Context, query string, queryS result, warnings, err := p.client.QueryRange(ctx, query, r, v1.WithTimeout(DefaultQueryTimeout)) duration := time.Since(start) if err != nil { - slog.Error("Backend call failed", "backend", p.backend, "operation", "range_query", - "duration_ms", duration.Milliseconds(), "query", query, "error", err) + klog.FromContext(ctx).Error(err, "Backend call failed", "backend", p.backend, "operation", "range_query", + "duration_ms", duration.Milliseconds(), "query", query) return nil, fmt.Errorf("error executing range query: %w", err) } - slog.Debug("Backend call completed", "backend", p.backend, "operation", "range_query", + klog.FromContext(ctx).V(4).Info("Backend call completed", "backend", p.backend, "operation", "range_query", "duration_ms", duration.Milliseconds(), "query", query) response := map[string]any{ @@ -201,11 +202,11 @@ func (p *RealLoader) ExecuteInstantQuery(ctx context.Context, query string, ts t result, warnings, err := p.client.Query(ctx, query, ts) duration := time.Since(start) if err != nil { - slog.Error("Backend call failed", "backend", p.backend, "operation", "instant_query", - "duration_ms", duration.Milliseconds(), "query", query, "error", err) + klog.FromContext(ctx).Error(err, "Backend call failed", "backend", p.backend, "operation", "instant_query", + "duration_ms", duration.Milliseconds(), "query", query) return nil, fmt.Errorf("error executing instant query: %w", err) } - slog.Debug("Backend call completed", "backend", p.backend, "operation", "instant_query", + klog.FromContext(ctx).V(4).Info("Backend call completed", "backend", p.backend, "operation", "instant_query", "duration_ms", duration.Milliseconds(), "query", query) response := map[string]any{ @@ -230,11 +231,11 @@ func (p *RealLoader) GetLabelNames(ctx context.Context, metricName string, start labelNames, _, err := p.client.LabelNames(ctx, matches, start, end) duration := time.Since(apiStart) if err != nil { - slog.Error("Backend call failed", "backend", p.backend, "operation", "label_names", - "duration_ms", duration.Milliseconds(), "error", err) + klog.FromContext(ctx).Error(err, "Backend call failed", "backend", p.backend, "operation", "label_names", + "duration_ms", duration.Milliseconds()) return nil, fmt.Errorf("error fetching label names: %w", err) } - slog.Debug("Backend call completed", "backend", p.backend, "operation", "label_names", + klog.FromContext(ctx).V(4).Info("Backend call completed", "backend", p.backend, "operation", "label_names", "duration_ms", duration.Milliseconds(), "result_count", len(labelNames)) labels := make([]string, len(labelNames)) @@ -252,11 +253,11 @@ func (p *RealLoader) GetLabelValues(ctx context.Context, label, metricName strin labelValues, _, err := p.client.LabelValues(ctx, label, matches, start, end) duration := time.Since(apiStart) if err != nil { - slog.Error("Backend call failed", "backend", p.backend, "operation", "label_values", - "duration_ms", duration.Milliseconds(), "label", label, "error", err) + klog.FromContext(ctx).Error(err, "Backend call failed", "backend", p.backend, "operation", "label_values", + "duration_ms", duration.Milliseconds(), "label", label) return nil, fmt.Errorf("error fetching label values: %w", err) } - slog.Debug("Backend call completed", "backend", p.backend, "operation", "label_values", + klog.FromContext(ctx).V(4).Info("Backend call completed", "backend", p.backend, "operation", "label_values", "duration_ms", duration.Milliseconds(), "label", label, "result_count", len(labelValues)) values := make([]string, len(labelValues)) @@ -271,11 +272,11 @@ func (p *RealLoader) GetSeries(ctx context.Context, matches []string, start, end seriesList, _, err := p.client.Series(ctx, matches, start, end) duration := time.Since(apiStart) if err != nil { - slog.Error("Backend call failed", "backend", p.backend, "operation", "series", - "duration_ms", duration.Milliseconds(), "error", err) + klog.FromContext(ctx).Error(err, "Backend call failed", "backend", p.backend, "operation", "series", + "duration_ms", duration.Milliseconds()) return nil, fmt.Errorf("error fetching series: %w", err) } - slog.Debug("Backend call completed", "backend", p.backend, "operation", "series", + klog.FromContext(ctx).V(4).Info("Backend call completed", "backend", p.backend, "operation", "series", "duration_ms", duration.Milliseconds(), "result_count", len(seriesList)) result := make([]map[string]string, len(seriesList)) diff --git a/pkg/tools/handlers.go b/pkg/tools/handlers.go index 24d69819..baaa322b 100644 --- a/pkg/tools/handlers.go +++ b/pkg/tools/handlers.go @@ -3,11 +3,12 @@ package tools import ( "context" "fmt" - "log/slog" "maps" "strings" "time" + "k8s.io/klog/v2" + ammodels "github.com/prometheus/alertmanager/api/v2/models" "github.com/prometheus/common/model" "k8s.io/utils/ptr" @@ -261,8 +262,8 @@ func BuildSilencesInput(args map[string]any) SilencesInput { // ListMetricsHandler handles the listing of available Prometheus metrics. func ListMetricsHandler(ctx context.Context, promClient prometheus.Loader, input ListMetricsInput) *resultutil.Result { - slog.Info("ListMetricsHandler called") - slog.Debug("ListMetricsHandler params", "input", input) + klog.FromContext(ctx).Info("ListMetricsHandler called") + klog.FromContext(ctx).V(4).Info("ListMetricsHandler params", "input", input) // Validate required parameters if input.NameRegex == "" { @@ -271,12 +272,12 @@ func ListMetricsHandler(ctx context.Context, promClient prometheus.Loader, input metrics, err := promClient.ListMetrics(ctx, input.NameRegex) if err != nil { - slog.Error("failed to list metrics", "error", err) + klog.FromContext(ctx).Error(err, "failed to list metrics") return resultutil.NewErrorResult(fmt.Errorf("failed to list metrics: %w", err)) } - slog.Info("ListMetricsHandler executed successfully", "resultLength", len(metrics)) - slog.Debug("ListMetricsHandler results", "results", metrics) + klog.FromContext(ctx).Info("ListMetricsHandler executed successfully", "resultLength", len(metrics)) + klog.FromContext(ctx).V(4).Info("ListMetricsHandler results", "results", metrics) output := ListMetricsOutput{Metrics: metrics} return resultutil.NewSuccessResult(output) @@ -284,8 +285,8 @@ func ListMetricsHandler(ctx context.Context, promClient prometheus.Loader, input // ExecuteRangeQueryHandler handles the execution of Prometheus range queries. func ExecuteRangeQueryHandler(ctx context.Context, promClient prometheus.Loader, input RangeQueryInput, fullResponse bool) *resultutil.Result { - slog.Info("ExecuteRangeQueryHandler called") - slog.Debug("ExecuteRangeQueryHandler params", "input", input) + klog.FromContext(ctx).Info("ExecuteRangeQueryHandler called") + klog.FromContext(ctx).V(4).Info("ExecuteRangeQueryHandler params", "input", input) // Validate required parameters if input.Query == "" { @@ -347,7 +348,7 @@ func ExecuteRangeQueryHandler(ctx context.Context, promClient prometheus.Loader, resMatrix, ok := result["result"].(model.Matrix) if ok { - slog.Info("ExecuteRangeQueryHandler executed successfully", "resultLength", resMatrix.Len()) + klog.FromContext(ctx).Info("ExecuteRangeQueryHandler executed successfully", "resultLength", resMatrix.Len()) if fullResponse { // Return full data @@ -374,9 +375,9 @@ func ExecuteRangeQueryHandler(ctx context.Context, promClient prometheus.Loader, } } - slog.Debug("ExecuteRangeQueryHandler output", "output", output) + klog.FromContext(ctx).V(4).Info("ExecuteRangeQueryHandler output", "output", output) } else { - slog.Info("ExecuteRangeQueryHandler executed successfully (unknown format)", "result", result) + klog.FromContext(ctx).Info("ExecuteRangeQueryHandler executed successfully (unknown format)", "result", result) } if warnings, ok := result["warnings"].([]string); ok { @@ -388,8 +389,8 @@ func ExecuteRangeQueryHandler(ctx context.Context, promClient prometheus.Loader, // ShowTimeseriesHandler handles the show_timeseries tool, returning full range query data for chart rendering. func ShowTimeseriesHandler(ctx context.Context, promClient prometheus.Loader, input ShowTimeseriesInput) *resultutil.Result { - slog.Info("ShowTimeseriesHandler called") - slog.Debug("ShowTimeseriesHandler params", "input", input) + klog.FromContext(ctx).Info("ShowTimeseriesHandler called") + klog.FromContext(ctx).V(4).Info("ShowTimeseriesHandler params", "input", input) // Executing the query handler just to validate the query is correct. result := ExecuteRangeQueryHandler(ctx, promClient, input.RangeQueryInput, true) @@ -402,8 +403,8 @@ func ShowTimeseriesHandler(ctx context.Context, promClient prometheus.Loader, in // ExecuteInstantQueryHandler handles the execution of Prometheus instant queries. func ExecuteInstantQueryHandler(ctx context.Context, promClient prometheus.Loader, input InstantQueryInput) *resultutil.Result { - slog.Info("ExecuteInstantQueryHandler called") - slog.Debug("ExecuteInstantQueryHandler params", "input", input) + klog.FromContext(ctx).Info("ExecuteInstantQueryHandler called") + klog.FromContext(ctx).V(4).Info("ExecuteInstantQueryHandler params", "input", input) // Validate required parameters if input.Query == "" { @@ -434,8 +435,8 @@ func ExecuteInstantQueryHandler(ctx context.Context, promClient prometheus.Loade resVector, ok := result["result"].(model.Vector) if ok { - slog.Info("ExecuteInstantQueryHandler executed successfully", "resultLength", len(resVector)) - slog.Debug("ExecuteInstantQueryHandler results", "results", resVector) + klog.FromContext(ctx).Info("ExecuteInstantQueryHandler executed successfully", "resultLength", len(resVector)) + klog.FromContext(ctx).V(4).Info("ExecuteInstantQueryHandler results", "results", resVector) output.Result = make([]InstantResult, len(resVector)) for i, sample := range resVector { @@ -449,7 +450,7 @@ func ExecuteInstantQueryHandler(ctx context.Context, promClient prometheus.Loade } } } else { - slog.Info("ExecuteInstantQueryHandler executed successfully (unknown format)", "result", result) + klog.FromContext(ctx).Info("ExecuteInstantQueryHandler executed successfully (unknown format)", "result", result) } if warnings, ok := result["warnings"].([]string); ok { @@ -461,8 +462,8 @@ func ExecuteInstantQueryHandler(ctx context.Context, promClient prometheus.Loade // GetLabelNamesHandler handles the retrieval of label names. func GetLabelNamesHandler(ctx context.Context, promClient prometheus.Loader, input LabelNamesInput) *resultutil.Result { - slog.Info("GetLabelNamesHandler called") - slog.Debug("GetLabelNamesHandler params", "input", input) + klog.FromContext(ctx).Info("GetLabelNamesHandler called") + klog.FromContext(ctx).V(4).Info("GetLabelNamesHandler params", "input", input) startTime, endTime, err := parseDefaultTimeRange(input.Start, input.End) if err != nil { @@ -475,8 +476,8 @@ func GetLabelNamesHandler(ctx context.Context, promClient prometheus.Loader, inp return resultutil.NewErrorResult(fmt.Errorf("failed to get label names: %w", err)) } - slog.Info("GetLabelNamesHandler executed successfully", "labelCount", len(labels)) - slog.Debug("GetLabelNamesHandler results", "results", labels) + klog.FromContext(ctx).Info("GetLabelNamesHandler executed successfully", "labelCount", len(labels)) + klog.FromContext(ctx).V(4).Info("GetLabelNamesHandler results", "results", labels) output := LabelNamesOutput{Labels: labels} return resultutil.NewSuccessResult(output) @@ -484,8 +485,8 @@ func GetLabelNamesHandler(ctx context.Context, promClient prometheus.Loader, inp // GetLabelValuesHandler handles the retrieval of label values. func GetLabelValuesHandler(ctx context.Context, promClient prometheus.Loader, input LabelValuesInput) *resultutil.Result { - slog.Info("GetLabelValuesHandler called") - slog.Debug("GetLabelValuesHandler params", "input", input) + klog.FromContext(ctx).Info("GetLabelValuesHandler called") + klog.FromContext(ctx).V(4).Info("GetLabelValuesHandler params", "input", input) // Validate required parameters if input.Label == "" { @@ -503,8 +504,8 @@ func GetLabelValuesHandler(ctx context.Context, promClient prometheus.Loader, in return resultutil.NewErrorResult(fmt.Errorf("failed to get label values: %w", err)) } - slog.Info("GetLabelValuesHandler executed successfully", "valueCount", len(values)) - slog.Debug("GetLabelValuesHandler results", "results", values) + klog.FromContext(ctx).Info("GetLabelValuesHandler executed successfully", "valueCount", len(values)) + klog.FromContext(ctx).V(4).Info("GetLabelValuesHandler results", "results", values) output := LabelValuesOutput{Values: values} return resultutil.NewSuccessResult(output) @@ -512,8 +513,8 @@ func GetLabelValuesHandler(ctx context.Context, promClient prometheus.Loader, in // GetSeriesHandler handles the retrieval of time series. func GetSeriesHandler(ctx context.Context, promClient prometheus.Loader, input SeriesInput) *resultutil.Result { - slog.Info("GetSeriesHandler called") - slog.Debug("GetSeriesHandler params", "input", input) + klog.FromContext(ctx).Info("GetSeriesHandler called") + klog.FromContext(ctx).V(4).Info("GetSeriesHandler params", "input", input) // Validate required parameters if input.Matches == "" { @@ -537,8 +538,8 @@ func GetSeriesHandler(ctx context.Context, promClient prometheus.Loader, input S return resultutil.NewErrorResult(fmt.Errorf("failed to get series: %w", err)) } - slog.Info("GetSeriesHandler executed successfully", "cardinality", len(series)) - slog.Debug("GetSeriesHandler results", "results", series) + klog.FromContext(ctx).Info("GetSeriesHandler executed successfully", "cardinality", len(series)) + klog.FromContext(ctx).V(4).Info("GetSeriesHandler results", "results", series) output := SeriesOutput{ Series: series, @@ -549,8 +550,8 @@ func GetSeriesHandler(ctx context.Context, promClient prometheus.Loader, input S // GetAlertsHandler handles the retrieval of alerts from Alertmanager. func GetAlertsHandler(ctx context.Context, amClient alertmanager.Loader, input AlertsInput) *resultutil.Result { - slog.Info("GetAlertsHandler called") - slog.Debug("GetAlertsHandler params", "input", input) + klog.FromContext(ctx).Info("GetAlertsHandler called") + klog.FromContext(ctx).V(4).Info("GetAlertsHandler params", "input", input) alerts, err := amClient.GetAlerts(ctx, input.Active, input.Silenced, input.Inhibited, input.Unprocessed, parseFilterString(input.Filter), input.Receiver) if err != nil { @@ -564,16 +565,16 @@ func GetAlertsHandler(ctx context.Context, amClient alertmanager.Loader, input A output.Alerts[i] = convertAlert(alert) } - slog.Info("GetAlertsHandler executed successfully", "alertCount", len(alerts)) - slog.Debug("GetAlertsHandler results", "results", output.Alerts) + klog.FromContext(ctx).Info("GetAlertsHandler executed successfully", "alertCount", len(alerts)) + klog.FromContext(ctx).V(4).Info("GetAlertsHandler results", "results", output.Alerts) return resultutil.NewSuccessResult(output) } // GetSilencesHandler handles the retrieval of silences from Alertmanager. func GetSilencesHandler(ctx context.Context, amClient alertmanager.Loader, input SilencesInput) *resultutil.Result { - slog.Info("GetSilencesHandler called") - slog.Debug("GetSilencesHandler params", "input", input) + klog.FromContext(ctx).Info("GetSilencesHandler called") + klog.FromContext(ctx).V(4).Info("GetSilencesHandler params", "input", input) silences, err := amClient.GetSilences(ctx, parseFilterString(input.Filter)) if err != nil { @@ -587,8 +588,8 @@ func GetSilencesHandler(ctx context.Context, amClient alertmanager.Loader, input output.Silences[i] = convertSilence(silence) } - slog.Info("GetSilencesHandler executed successfully", "silenceCount", len(silences)) - slog.Debug("GetSilencesHandler results", "results", output.Silences) + klog.FromContext(ctx).Info("GetSilencesHandler executed successfully", "silenceCount", len(silences)) + klog.FromContext(ctx).V(4).Info("GetSilencesHandler results", "results", output.Silences) return resultutil.NewSuccessResult(output) } diff --git a/pkg/toolset/tools/prometheus_client.go b/pkg/toolset/tools/prometheus_client.go index 758e98fe..e51dc59e 100644 --- a/pkg/toolset/tools/prometheus_client.go +++ b/pkg/toolset/tools/prometheus_client.go @@ -2,9 +2,10 @@ package tools import ( "fmt" - "log/slog" "strings" + "k8s.io/klog/v2" + "github.com/containers/kubernetes-mcp-server/pkg/api" promapi "github.com/prometheus/client_golang/api" @@ -37,13 +38,13 @@ func getPromClient(params api.ToolHandlerParams) (prometheus.Loader, error) { metricsBackendURL := cfg.PrometheusURL if metricsBackendURL == "" { metricsBackendURL = defaultPrometheusURL - slog.Info("No prometheus_url configured, using default", "url", defaultPrometheusURL) + klog.FromContext(params.Context).Info("No prometheus_url configured, using default", "url", defaultPrometheusURL) } // Get guardrails configuration guardrails, err := cfg.GetGuardrails() if err != nil { - slog.Warn("Failed to parse guardrails configuration", "err", err) + klog.FromContext(params.Context).Info("Failed to parse guardrails configuration", "err", err) } apiConfig, err := buildAPIConfig(params, metricsBackendURL, cfg.Insecure, cfg.GetAuthMode()) diff --git a/tests/e2e/openshift_e2e_test.go b/tests/e2e/openshift_e2e_test.go index 2bfffb8e..3e671276 100644 --- a/tests/e2e/openshift_e2e_test.go +++ b/tests/e2e/openshift_e2e_test.go @@ -3,6 +3,7 @@ package e2e import ( + "context" "encoding/json" "net/http" "os" @@ -21,7 +22,7 @@ import ( // TestRouteDiscovery_ThanosQuerier verifies that the thanos-querier route in // openshift-monitoring can be discovered and returns a valid https:// URL. func TestRouteDiscovery_ThanosQuerier(t *testing.T) { - discoveredURL, err := k8s.GetMetricsBackendURL(k8s.MetricsBackendThanos) + discoveredURL, err := k8s.GetMetricsBackendURL(context.Background(), k8s.MetricsBackendThanos) if err != nil { t.Fatalf("Failed to discover thanos-querier route: %v", err) } @@ -32,7 +33,7 @@ func TestRouteDiscovery_ThanosQuerier(t *testing.T) { // TestRouteDiscovery_PrometheusK8s verifies that the prometheus-k8s route in // openshift-monitoring can be discovered when using the prometheus backend. func TestRouteDiscovery_PrometheusK8s(t *testing.T) { - discoveredURL, err := k8s.GetMetricsBackendURL(k8s.MetricsBackendPrometheus) + discoveredURL, err := k8s.GetMetricsBackendURL(context.Background(), k8s.MetricsBackendPrometheus) if err != nil { t.Fatalf("Failed to discover prometheus-k8s route: %v", err) } @@ -43,7 +44,7 @@ func TestRouteDiscovery_PrometheusK8s(t *testing.T) { // TestRouteDiscovery_Alertmanager verifies that the alertmanager-main route in // openshift-monitoring can be discovered and returns a valid https:// URL. func TestRouteDiscovery_Alertmanager(t *testing.T) { - discoveredURL, err := k8s.GetAlertmanagerURL() + discoveredURL, err := k8s.GetAlertmanagerURL(context.Background()) if err != nil { t.Fatalf("Failed to discover alertmanager-main route: %v", err) } @@ -61,17 +62,17 @@ func TestRouteDiscovery_URLsAreReachable(t *testing.T) { }{ { name: "thanos-querier", - getURL: func() (string, error) { return k8s.GetMetricsBackendURL(k8s.MetricsBackendThanos) }, + getURL: func() (string, error) { return k8s.GetMetricsBackendURL(context.Background(), k8s.MetricsBackendThanos) }, apiPath: "/api/v1/query?query=up", }, { name: "prometheus-k8s", - getURL: func() (string, error) { return k8s.GetMetricsBackendURL(k8s.MetricsBackendPrometheus) }, + getURL: func() (string, error) { return k8s.GetMetricsBackendURL(context.Background(), k8s.MetricsBackendPrometheus) }, apiPath: "/api/v1/query?query=up", }, { name: "alertmanager-main", - getURL: k8s.GetAlertmanagerURL, + getURL: func() (string, error) { return k8s.GetAlertmanagerURL(context.Background()) }, apiPath: "/api/v2/status", }, }