From f7184e49e4568b251561615bac64ace7ca82c19b Mon Sep 17 00:00:00 2001 From: Adarsh Kumar Yadav Date: Wed, 15 Jul 2026 11:02:56 +0530 Subject: [PATCH 1/3] feat: add Grafana dashboard for operator observability Ship an importable Grafana dashboard and optional sidecar provisioning overlay so cluster operators can visualize mcpserver_* and controller-runtime metrics introduced in #100. --- Makefile | 7 +- README.md | 2 +- config/grafana/dashboard_test.go | 176 +++ config/grafana/kustomization.yaml | 15 + config/grafana/mcp-lifecycle-operator.json | 1243 ++++++++++++++++++++ hack/verify-grafana-dashboard.sh | 47 + site-src/operating/metrics.md | 31 + 7 files changed, 1519 insertions(+), 2 deletions(-) create mode 100644 config/grafana/dashboard_test.go create mode 100644 config/grafana/kustomization.yaml create mode 100644 config/grafana/mcp-lifecycle-operator.json create mode 100755 hack/verify-grafana-dashboard.sh diff --git a/Makefile b/Makefile index 8e17034e..328dcaac 100644 --- a/Makefile +++ b/Makefile @@ -74,8 +74,13 @@ COVER_PROFILE ?= cover.out # Human-readable reports (not used by CI; see kubernetes-sigs/cluster-api `test-cover` pattern). COVER_OUTPUT_DIR ?= out +.PHONY: verify-grafana-dashboard +verify-grafana-dashboard: ## Validate Grafana dashboard JSON and PromQL references. + chmod +x hack/verify-grafana-dashboard.sh + ./hack/verify-grafana-dashboard.sh + .PHONY: test -test: manifests generate fmt vet setup-envtest ## Run tests. +test: manifests generate fmt vet setup-envtest verify-grafana-dashboard ## Run tests. KUBEBUILDER_ASSETS="$(shell "$(ENVTEST)" use $(ENVTEST_K8S_VERSION) --bin-dir "$(LOCALBIN)" -p path)" \ go test $$(go list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' ./... | grep -v /e2e) -coverprofile $(COVER_PROFILE) diff --git a/README.md b/README.md index bc7391ab..b03c1b78 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A Kubernetes operator that provides a declarative API to deploy, manage, and saf - [Introduction](https://mcp-lifecycle-operator.sigs.k8s.io/introduction/) - Architecture and MCPServer API overview - [Quickstart Guide](https://mcp-lifecycle-operator.sigs.k8s.io/guides/quickstart/) - Get up and running quickly -- [Metrics](https://mcp-lifecycle-operator.sigs.k8s.io/operating/metrics/) - Prometheus metrics reference +- [Metrics](https://mcp-lifecycle-operator.sigs.k8s.io/operating/metrics/) - Prometheus metrics reference and Grafana dashboard - [API Reference](https://mcp-lifecycle-operator.sigs.k8s.io/reference/) - Full MCPServer API documentation - [Complete MCPServer example](./config/samples/mcp_v1alpha1_mcpserver_complete.yaml) - YAML showing all available fields - [Contributing](https://mcp-lifecycle-operator.sigs.k8s.io/contributing/) - How to contribute to the project diff --git a/config/grafana/dashboard_test.go b/config/grafana/dashboard_test.go new file mode 100644 index 00000000..6140e74f --- /dev/null +++ b/config/grafana/dashboard_test.go @@ -0,0 +1,176 @@ +/* +Copyright 2026 The Kubernetes Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package grafana_test + +import ( + "encoding/json" + "os" + "path/filepath" + "regexp" + "strings" + "testing" +) + +const dashboardFile = "mcp-lifecycle-operator.json" + +// knownMetrics lists metric selectors the dashboard is allowed to reference. +// Keep in sync with internal/controller/metrics.go and controller-runtime defaults. +var knownMetrics = []string{ + "mcpserver_condition_info", + "mcpserver_validation_failures_total", + "mcpserver_deployment_failures_total", + "mcpserver_service_failures_total", + "mcpserver_networkpolicy_failures_total", + "mcpserver_reconcile_phase_duration_seconds", + "mcpserver_reconcile_phase_duration_seconds_bucket", + "controller_runtime_reconcile_total", + "controller_runtime_reconcile_errors_total", + "controller_runtime_reconcile_time_seconds_bucket", + "controller_runtime_active_workers", + "process_cpu_seconds_total", + "process_resident_memory_bytes", + "rest_client_requests_total", +} + +func TestDashboardJSONIsValid(t *testing.T) { + path := filepath.Join(".", dashboardFile) + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read dashboard: %v", err) + } + + var root map[string]any + if err := json.Unmarshal(data, &root); err != nil { + t.Fatalf("dashboard is not valid JSON: %v", err) + } + + for _, key := range []string{"title", "uid", "panels", "tags"} { + if _, ok := root[key]; !ok { + t.Errorf("dashboard missing required field %q", key) + } + } + + if root["uid"] != "mcp-lifecycle-operator" { + t.Errorf("expected uid mcp-lifecycle-operator, got %v", root["uid"]) + } + + panels, ok := root["panels"].([]any) + if !ok || len(panels) == 0 { + t.Fatal("dashboard must contain panels") + } +} + +func TestDashboardQueriesReferenceKnownMetrics(t *testing.T) { + path := filepath.Join(".", dashboardFile) + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read dashboard: %v", err) + } + content := string(data) + + exprs := extractExprs(data) + if len(exprs) == 0 { + t.Fatal("no PromQL expressions found in dashboard") + } + + required := []string{ + "mcpserver_condition_info", + "mcpserver_validation_failures_total", + "mcpserver_deployment_failures_total", + "mcpserver_service_failures_total", + "mcpserver_networkpolicy_failures_total", + "mcpserver_reconcile_phase_duration_seconds", + "controller_runtime_reconcile_total", + "controller_runtime_reconcile_errors_total", + "controller_runtime_reconcile_time_seconds", + } + for _, metric := range required { + if !strings.Contains(content, metric) { + t.Errorf("dashboard missing required metric reference %q", metric) + } + } + + unimplemented := []string{ + "referenced_resources_count", + "mcpserver_configmap", + "mcpserver_secret", + } + for _, metric := range unimplemented { + if strings.Contains(content, metric) { + t.Errorf("dashboard references unimplemented metric %q", metric) + } + } + + selectorRE := regexp.MustCompile(`([a-zA-Z_:][a-zA-Z0-9_:]*)\s*[\{\[]`) + seen := map[string]struct{}{} + for _, expr := range exprs { + for _, match := range selectorRE.FindAllStringSubmatch(expr, -1) { + seen[match[1]] = struct{}{} + } + } + + allowed := make(map[string]struct{}, len(knownMetrics)) + for _, m := range knownMetrics { + allowed[m] = struct{}{} + } + for name := range seen { + if _, ok := allowed[name]; !ok { + t.Errorf("expression uses unexpected metric selector %q", name) + } + } +} + +func TestDashboardKustomizationBuilds(t *testing.T) { + path := filepath.Join(".", dashboardFile) + if _, err := os.Stat(path); err != nil { + t.Fatalf("dashboard file missing for kustomize: %v", err) + } +} + +func extractExprs(data []byte) []string { + var root any + if err := json.Unmarshal(data, &root); err != nil { + return nil + } + var exprs []string + walkJSON(root, func(key string, value string) { + if key == "expr" { + if strings.TrimSpace(value) != "" { + exprs = append(exprs, value) + } + } + }) + return exprs +} + +func walkJSON(node any, fn func(key, value string)) { + switch v := node.(type) { + case map[string]any: + for k, child := range v { + switch c := child.(type) { + case string: + fn(k, c) + default: + walkJSON(c, fn) + } + } + case []any: + for _, child := range v { + walkJSON(child, fn) + } + } +} diff --git a/config/grafana/kustomization.yaml b/config/grafana/kustomization.yaml new file mode 100644 index 00000000..556fa19e --- /dev/null +++ b/config/grafana/kustomization.yaml @@ -0,0 +1,15 @@ +# Optional Grafana dashboard provisioning for environments that use the +# Grafana Helm chart sidecar (label grafana_dashboard: "1"). +# Apply with: kubectl apply -k config/grafana/ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +configMapGenerator: + - name: grafana-dashboard-mcp-lifecycle-operator + files: + - mcp-lifecycle-operator.json + options: + labels: + grafana_dashboard: "1" + annotations: + grafana_folder: MCP Lifecycle Operator diff --git a/config/grafana/mcp-lifecycle-operator.json b/config/grafana/mcp-lifecycle-operator.json new file mode 100644 index 00000000..f700b124 --- /dev/null +++ b/config/grafana/mcp-lifecycle-operator.json @@ -0,0 +1,1243 @@ +{ + "__inputs": [ + { + "name": "DS_PROMETHEUS", + "label": "Prometheus", + "description": "Prometheus datasource scraping the MCP Lifecycle Operator metrics endpoint", + "type": "datasource", + "pluginId": "prometheus", + "pluginName": "Prometheus" + } + ], + "__requires": [ + { + "type": "grafana", + "id": "grafana", + "name": "Grafana", + "version": "10.0.0" + }, + { + "type": "datasource", + "id": "prometheus", + "name": "Prometheus", + "version": "1.0.0" + }, + { + "type": "panel", + "id": "timeseries", + "name": "Time series", + "version": "" + }, + { + "type": "panel", + "id": "stat", + "name": "Stat", + "version": "" + }, + { + "type": "panel", + "id": "piechart", + "name": "Pie chart", + "version": "" + }, + { + "type": "panel", + "id": "heatmap", + "name": "Heatmap", + "version": "" + } + ], + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 6380, + "panels": [], + "title": "Overview", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 1, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "count(mcpserver_condition_info{type=\"Ready\", namespace=~\"$mcpserver_namespace\"})", + "range": true, + "refId": "A" + } + ], + "title": "MCPServers", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 6, + "y": 1 + }, + "id": 2, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "100 * sum(rate(controller_runtime_reconcile_total{controller=\"mcpserver\",result=\"success\"}[5m])) / clamp_min(sum(rate(controller_runtime_reconcile_total{controller=\"mcpserver\"}[5m])), 1e-9)", + "range": true, + "refId": "A" + } + ], + "title": "Reconcile success rate", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 12, + "y": 1 + }, + "id": 3, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(rate(controller_runtime_reconcile_errors_total{controller=\"mcpserver\"}[5m]))", + "range": true, + "refId": "A" + } + ], + "title": "Reconcile error rate", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 6, + "x": 18, + "y": 1 + }, + "id": 4, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "10.4.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(controller_runtime_reconcile_time_seconds_bucket{controller=\"mcpserver\"}[5m])) by (le))", + "range": true, + "refId": "A" + } + ], + "title": "P95 reconcile latency", + "type": "stat" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 5 + }, + "id": 6018, + "panels": [], + "title": "Reconciliation", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "custom": { + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + } + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 6 + }, + "id": 10, + "options": { + "calculate": false, + "cellGap": 1, + "color": { + "mode": "scheme", + "scheme": "Spectral" + }, + "yAxis": { + "unit": "s" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(increase(controller_runtime_reconcile_time_seconds_bucket{controller=\"mcpserver\"}[5m])) by (le)", + "range": true, + "refId": "A" + } + ], + "title": "Reconciliation duration", + "type": "heatmap" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "none" + } + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 6 + }, + "id": 11, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(rate(controller_runtime_reconcile_total{controller=\"mcpserver\"}[5m])) by (result)", + "range": true, + "refId": "A", + "legendFormat": "{{result}}" + } + ], + "title": "Reconciliation rate by result", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "none" + } + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 14 + }, + "id": 12, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(rate(controller_runtime_reconcile_total{controller=\"mcpserver\",result=~\"requeue.*\"}[5m]))", + "range": true, + "refId": "A", + "legendFormat": "{{result}}" + } + ], + "title": "Requeue rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "none" + } + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 14 + }, + "id": 13, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.99, sum(rate(mcpserver_reconcile_phase_duration_seconds_bucket[5m])) by (le, phase))", + "range": true, + "refId": "A", + "legendFormat": "{{phase}}" + } + ], + "title": "Phase duration P99", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 8273, + "panels": [], + "title": "Resource health", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 21 + }, + "id": 20, + "options": { + "legend": { + "displayMode": "table", + "placement": "right" + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ] + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum by (status) (mcpserver_condition_info{type=\"Accepted\", namespace=~\"$mcpserver_namespace\"})", + "range": true, + "refId": "A", + "legendFormat": "{{status}}" + } + ], + "title": "MCPServers by Accepted status", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 21 + }, + "id": 21, + "options": { + "legend": { + "displayMode": "table", + "placement": "right" + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ] + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum by (status) (mcpserver_condition_info{type=\"Ready\", namespace=~\"$mcpserver_namespace\"})", + "range": true, + "refId": "A", + "legendFormat": "{{status}}" + } + ], + "title": "MCPServers by Ready status", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "none" + } + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 21 + }, + "id": 22, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(rate(mcpserver_deployment_failures_total{namespace=~\"$mcpserver_namespace\"}[5m])) by (namespace)", + "range": true, + "refId": "A", + "legendFormat": "deployment {{namespace}}" + } + ], + "title": "Reconciliation failures (rate)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "none" + } + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 0, + "y": 29 + }, + "id": 23, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(rate(mcpserver_service_failures_total{namespace=~\"$mcpserver_namespace\"}[5m])) by (namespace)", + "range": true, + "refId": "A", + "legendFormat": "{{namespace}}" + } + ], + "title": "Service failures (rate)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "none" + } + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 12, + "x": 12, + "y": 29 + }, + "id": 24, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(rate(mcpserver_networkpolicy_failures_total{namespace=~\"$mcpserver_namespace\"}[5m])) by (namespace)", + "range": true, + "refId": "A", + "legendFormat": "{{namespace}}" + } + ], + "title": "NetworkPolicy failures (rate)", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 35 + }, + "id": 6239, + "panels": [], + "title": "Validation", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "mappings": [] + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 36 + }, + "id": 30, + "options": { + "legend": { + "displayMode": "table", + "placement": "right" + }, + "pieType": "pie", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ] + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum by (reason) (mcpserver_validation_failures_total{namespace=~\"$mcpserver_namespace\"})", + "range": true, + "refId": "A", + "legendFormat": "{{status}}" + } + ], + "title": "Validation failures by reason", + "type": "piechart" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "none" + } + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 36 + }, + "id": 31, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(rate(mcpserver_validation_failures_total{namespace=~\"$mcpserver_namespace\"}[5m])) by (namespace, reason)", + "range": true, + "refId": "A", + "legendFormat": "{{namespace}}/{{reason}}" + } + ], + "title": "Validation failure rate", + "type": "timeseries" + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 3059, + "panels": [], + "title": "Performance", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "none" + } + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 45 + }, + "id": 40, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(rate(process_cpu_seconds_total[5m]))", + "range": true, + "refId": "A", + "legendFormat": "CPU" + } + ], + "title": "Operator CPU usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "none" + } + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 45 + }, + "id": 41, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(process_resident_memory_bytes)", + "range": true, + "refId": "A", + "legendFormat": "memory" + } + ], + "title": "Operator memory (RSS)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "none" + } + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 12, + "y": 45 + }, + "id": 42, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "controller_runtime_active_workers{controller=\"mcpserver\"}", + "range": true, + "refId": "A", + "legendFormat": "workers" + } + ], + "title": "Active reconcile workers", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "drawStyle": "line", + "fillOpacity": 10, + "lineWidth": 1, + "showPoints": "never", + "stacking": { + "group": "A", + "mode": "none" + } + }, + "unit": "ops" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 18, + "y": 45 + }, + "id": 43, + "options": { + "legend": { + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "editorMode": "code", + "expr": "sum(rate(rest_client_requests_total[5m])) by (code)", + "range": true, + "refId": "A", + "legendFormat": "{{code}}" + } + ], + "title": "API server request rate", + "type": "timeseries" + } + ], + "refresh": "30s", + "schemaVersion": 39, + "tags": [ + "mcp-lifecycle-operator", + "kubernetes", + "controller-runtime" + ], + "templating": { + "list": [ + { + "current": { + "selected": true, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${DS_PROMETHEUS}" + }, + "definition": "label_values(mcpserver_condition_info, namespace)", + "hide": 0, + "includeAll": true, + "label": "MCPServer namespace", + "multi": true, + "name": "mcpserver_namespace", + "options": [], + "query": { + "query": "label_values(mcpserver_condition_info, namespace)", + "refId": "PrometheusVariableQuery" + }, + "refresh": 2, + "regex": "", + "sort": 1, + "type": "query" + } + ] + }, + "time": { + "from": "now-1h", + "to": "now" + }, + "timepicker": {}, + "timezone": "browser", + "title": "MCP Lifecycle Operator", + "uid": "mcp-lifecycle-operator", + "version": 1, + "description": "Observability dashboard for the MCP Lifecycle Operator controller manager. Covers controller-runtime reconciliation metrics and custom mcpserver_* series." +} diff --git a/hack/verify-grafana-dashboard.sh b/hack/verify-grafana-dashboard.sh new file mode 100755 index 00000000..63a72011 --- /dev/null +++ b/hack/verify-grafana-dashboard.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Copyright 2026 The Kubernetes Authors. +# +# Validates the Grafana dashboard JSON and optionally checks that PromQL queries +# return data from a live Prometheus instance (PROMETHEUS_URL). +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +DASHBOARD="${ROOT}/config/grafana/mcp-lifecycle-operator.json" + +echo "==> Validating dashboard JSON structure" +python3 - "${DASHBOARD}" <<'PY' +import json, sys +path = sys.argv[1] +with open(path) as f: + d = json.load(f) +assert d.get("uid") == "mcp-lifecycle-operator", "unexpected uid" +assert d.get("title"), "missing title" +assert d.get("panels"), "missing panels" +print(f"OK: {path} ({len(d['panels'])} top-level panels, uid={d['uid']})") +PY + +echo "==> Running Go dashboard unit tests" +(cd "${ROOT}/config/grafana" && go test -v ./...) + +if [[ -n "${PROMETHEUS_URL:-}" ]]; then + echo "==> Querying live Prometheus at ${PROMETHEUS_URL}" + queries=( + 'mcpserver_condition_info' + 'controller_runtime_reconcile_total{controller="mcpserver"}' + 'controller_runtime_reconcile_time_seconds_bucket{controller="mcpserver"}' + 'process_cpu_seconds_total' + ) + for q in "${queries[@]}"; do + encoded="$(python3 -c "import urllib.parse; print(urllib.parse.quote('''${q}'''))")" + result="$(curl -sf "${PROMETHEUS_URL}/api/v1/query?query=${encoded}")" + count="$(echo "${result}" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('data',{}).get('result',[])))")" + echo " query=${q} series=${count}" + if [[ "${count}" -eq 0 ]]; then + echo "WARN: no series for ${q}" >&2 + fi + done +else + echo "==> Skipping live Prometheus checks (set PROMETHEUS_URL to enable)" +fi + +echo "==> Dashboard verification complete" diff --git a/site-src/operating/metrics.md b/site-src/operating/metrics.md index a0d3244b..d4175871 100644 --- a/site-src/operating/metrics.md +++ b/site-src/operating/metrics.md @@ -141,6 +141,37 @@ spec: The repository maintains the full sample at [`config/prometheus/monitor.yaml`](https://github.com/kubernetes-sigs/mcp-lifecycle-operator/blob/main/config/prometheus/monitor.yaml). Wire it into your install by uncommenting the **`[PROMETHEUS]`** resource (`../prometheus`) in [`config/default/kustomization.yaml`](https://github.com/kubernetes-sigs/mcp-lifecycle-operator/blob/main/config/default/kustomization.yaml), or apply an equivalent manifest alongside kube-prometheus-stack. Add labels your Prometheus `ServiceMonitor` selector expects (for example `release: prometheus`). +## Grafana dashboard + +The repository ships an example Grafana dashboard at [`config/grafana/mcp-lifecycle-operator.json`](https://github.com/kubernetes-sigs/mcp-lifecycle-operator/blob/main/config/grafana/mcp-lifecycle-operator.json). It visualizes: + +- **Overview** — `MCPServer` count, reconciliation success rate, error rate, and P95 latency +- **Reconciliation** — duration heatmap, rate by result, requeue rate, and per-phase latency +- **Resource health** — Accepted/Ready condition breakdown and deployment, Service, and NetworkPolicy failure rates +- **Validation** — validation failures by reason +- **Performance** — operator CPU/memory, active reconcile workers, and API server request rate + +Panels use only metrics that the operator exports today (`mcpserver_*` and controller-runtime `controller_runtime_*` series). ConfigMap/Secret watch metrics from early design notes are **not** included because they are not implemented. + +### Import via Grafana UI + +1. Open your Grafana instance and go to **Dashboards → New → Import**. +2. Upload [`config/grafana/mcp-lifecycle-operator.json`](https://github.com/kubernetes-sigs/mcp-lifecycle-operator/blob/main/config/grafana/mcp-lifecycle-operator.json), or paste the [raw GitHub URL](https://raw.githubusercontent.com/kubernetes-sigs/mcp-lifecycle-operator/main/config/grafana/mcp-lifecycle-operator.json). +3. Select your Prometheus data source when prompted (`DS_PROMETHEUS`). +4. Save the dashboard. + +This follows the same **raw JSON + manual import** pattern used by [node-feature-discovery](https://github.com/kubernetes-sigs/node-feature-discovery/blob/master/docs/deployment/metrics.md) and the [Kubebuilder Grafana plugin](https://book.kubebuilder.io/plugins/available/grafana-v1-alpha.html). + +### Provision with Grafana sidecar (optional) + +If you run Grafana with the [kiwigrid sidecar](https://github.com/grafana/helm-charts/tree/main/charts/grafana) (for example via [kube-prometheus-stack](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack)), apply the labeled ConfigMap: + +```bash +kubectl apply -k config/grafana/ +``` + +The Kustomize overlay sets `grafana_dashboard: "1"` and `grafana_folder: MCP Lifecycle Operator`, matching conventions from [Strimzi](https://github.com/strimzi/strimzi-kafka-operator) and kube-prometheus-stack. + ## Next steps - **[Introduction](../introduction.md)** — Architecture and `MCPServer` overview (including status conditions) From 429d7bac6808eba3ebf05da8e8966f2281b537d2 Mon Sep 17 00:00:00 2001 From: Adarsh Kumar Yadav Date: Wed, 15 Jul 2026 11:39:09 +0530 Subject: [PATCH 2/3] Address CodeRabbit review feedback on Grafana dashboard. Use PromQL parser validation, real kustomize build tests, datasource template variables, corrected panel units/legends, operator namespace scoping, curl timeouts, and expanded metrics documentation. --- config/grafana/dashboard_test.go | 100 +++++++++++- config/grafana/kustomization.yaml | 9 +- config/grafana/mcp-lifecycle-operator.json | 167 +++++++++------------ go.mod | 5 + go.sum | 55 +++++++ hack/verify-grafana-dashboard.sh | 2 +- site-src/operating/metrics.md | 12 +- 7 files changed, 238 insertions(+), 112 deletions(-) diff --git a/config/grafana/dashboard_test.go b/config/grafana/dashboard_test.go index 6140e74f..fe62b362 100644 --- a/config/grafana/dashboard_test.go +++ b/config/grafana/dashboard_test.go @@ -19,10 +19,13 @@ package grafana_test import ( "encoding/json" "os" + "os/exec" "path/filepath" - "regexp" "strings" "testing" + + "github.com/prometheus/prometheus/promql/parser" + "gopkg.in/yaml.v3" ) const dashboardFile = "mcp-lifecycle-operator.json" @@ -68,6 +71,10 @@ func TestDashboardJSONIsValid(t *testing.T) { t.Errorf("expected uid mcp-lifecycle-operator, got %v", root["uid"]) } + if _, ok := root["__inputs"]; ok { + t.Error("dashboard must not use __inputs; use the datasource template variable for provisioning") + } + panels, ok := root["panels"].([]any) if !ok || len(panels) == 0 { t.Fatal("dashboard must contain panels") @@ -115,11 +122,14 @@ func TestDashboardQueriesReferenceKnownMetrics(t *testing.T) { } } - selectorRE := regexp.MustCompile(`([a-zA-Z_:][a-zA-Z0-9_:]*)\s*[\{\[]`) seen := map[string]struct{}{} for _, expr := range exprs { - for _, match := range selectorRE.FindAllStringSubmatch(expr, -1) { - seen[match[1]] = struct{}{} + names, err := metricSelectorsFromExpr(expr) + if err != nil { + t.Fatalf("parse expr %q: %v", expr, err) + } + for _, name := range names { + seen[name] = struct{}{} } } @@ -135,10 +145,86 @@ func TestDashboardQueriesReferenceKnownMetrics(t *testing.T) { } func TestDashboardKustomizationBuilds(t *testing.T) { - path := filepath.Join(".", dashboardFile) - if _, err := os.Stat(path); err != nil { - t.Fatalf("dashboard file missing for kustomize: %v", err) + t.Helper() + + kustomize := filepath.Join("..", "..", "bin", "kustomize") + if _, err := os.Stat(kustomize); err != nil { + t.Fatalf("kustomize binary missing at %s: %v", kustomize, err) + } + + cmd := exec.Command(kustomize, "build", ".") + cmd.Dir = "." + out, err := cmd.Output() + if err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + t.Fatalf("kustomize build failed: %v\n%s", err, string(exitErr.Stderr)) + } + t.Fatalf("kustomize build failed: %v", err) + } + + docs := splitYAMLDocuments(string(out)) + if len(docs) == 0 { + t.Fatal("kustomize build produced no documents") + } + + var found bool + for _, doc := range docs { + var obj map[string]any + if err := yaml.Unmarshal([]byte(doc), &obj); err != nil { + t.Fatalf("parse kustomize output: %v", err) + } + if obj["kind"] != "ConfigMap" { + continue + } + meta, _ := obj["metadata"].(map[string]any) + labels, _ := meta["labels"].(map[string]any) + if labels["grafana_dashboard"] != "1" { + t.Errorf("expected grafana_dashboard=1 label, got %#v", labels) + } + data, _ := obj["data"].(map[string]any) + raw, ok := data["mcp-lifecycle-operator.json"].(string) + if !ok || raw == "" { + t.Fatal("ConfigMap missing mcp-lifecycle-operator.json data") + } + var dash map[string]any + if err := json.Unmarshal([]byte(raw), &dash); err != nil { + t.Fatalf("embedded dashboard is not valid JSON: %v", err) + } + if dash["uid"] != "mcp-lifecycle-operator" { + t.Errorf("embedded dashboard uid = %v, want mcp-lifecycle-operator", dash["uid"]) + } + found = true + } + if !found { + t.Fatal("kustomize build did not produce Grafana dashboard ConfigMap") + } +} + +func metricSelectorsFromExpr(expr string) ([]string, error) { + parsed, err := parser.ParseExpr(expr) + if err != nil { + return nil, err + } + + var names []string + parser.Inspect(parsed, func(node parser.Node, _ []parser.Node) error { + if vs, ok := node.(*parser.VectorSelector); ok && vs.Name != "" { + names = append(names, vs.Name) + } + return nil + }) + return names, nil +} + +func splitYAMLDocuments(raw string) []string { + var docs []string + for _, part := range strings.Split(raw, "\n---") { + part = strings.TrimSpace(part) + if part != "" { + docs = append(docs, part) + } } + return docs } func extractExprs(data []byte) []string { diff --git a/config/grafana/kustomization.yaml b/config/grafana/kustomization.yaml index 556fa19e..8447b1f4 100644 --- a/config/grafana/kustomization.yaml +++ b/config/grafana/kustomization.yaml @@ -1,6 +1,13 @@ # Optional Grafana dashboard provisioning for environments that use the # Grafana Helm chart sidecar (label grafana_dashboard: "1"). -# Apply with: kubectl apply -k config/grafana/ +# +# Apply from a repository checkout: +# kubectl apply -k config/grafana/ +# +# Note: the grafana_folder annotation is honored only when the Grafana chart sets +# sidecar.dashboards.folderAnnotation: grafana_folder +# sidecar.dashboards.provider.foldersFromFilesStructure: true +# Without those values, dashboards are still imported but may land in the default folder. apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization diff --git a/config/grafana/mcp-lifecycle-operator.json b/config/grafana/mcp-lifecycle-operator.json index f700b124..9df9d3a8 100644 --- a/config/grafana/mcp-lifecycle-operator.json +++ b/config/grafana/mcp-lifecycle-operator.json @@ -1,52 +1,4 @@ { - "__inputs": [ - { - "name": "DS_PROMETHEUS", - "label": "Prometheus", - "description": "Prometheus datasource scraping the MCP Lifecycle Operator metrics endpoint", - "type": "datasource", - "pluginId": "prometheus", - "pluginName": "Prometheus" - } - ], - "__requires": [ - { - "type": "grafana", - "id": "grafana", - "name": "Grafana", - "version": "10.0.0" - }, - { - "type": "datasource", - "id": "prometheus", - "name": "Prometheus", - "version": "1.0.0" - }, - { - "type": "panel", - "id": "timeseries", - "name": "Time series", - "version": "" - }, - { - "type": "panel", - "id": "stat", - "name": "Stat", - "version": "" - }, - { - "type": "panel", - "id": "piechart", - "name": "Pie chart", - "version": "" - }, - { - "type": "panel", - "id": "heatmap", - "name": "Heatmap", - "version": "" - } - ], "annotations": { "list": [ { @@ -86,7 +38,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -133,7 +85,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "count(mcpserver_condition_info{type=\"Ready\", namespace=~\"$mcpserver_namespace\"})", @@ -147,7 +99,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -194,7 +146,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "100 * sum(rate(controller_runtime_reconcile_total{controller=\"mcpserver\",result=\"success\"}[5m])) / clamp_min(sum(rate(controller_runtime_reconcile_total{controller=\"mcpserver\"}[5m])), 1e-9)", @@ -208,7 +160,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -255,7 +207,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(rate(controller_runtime_reconcile_errors_total{controller=\"mcpserver\"}[5m]))", @@ -269,7 +221,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -316,7 +268,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "histogram_quantile(0.95, sum(rate(controller_runtime_reconcile_time_seconds_bucket{controller=\"mcpserver\"}[5m])) by (le))", @@ -343,7 +295,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -379,7 +331,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(increase(controller_runtime_reconcile_time_seconds_bucket{controller=\"mcpserver\"}[5m])) by (le)", @@ -393,7 +345,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -434,7 +386,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(rate(controller_runtime_reconcile_total{controller=\"mcpserver\"}[5m])) by (result)", @@ -449,7 +401,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -490,7 +442,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(rate(controller_runtime_reconcile_total{controller=\"mcpserver\",result=~\"requeue.*\"}[5m]))", @@ -505,7 +457,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -546,7 +498,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "histogram_quantile(0.99, sum(rate(mcpserver_reconcile_phase_duration_seconds_bucket[5m])) by (le, phase))", @@ -574,7 +526,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -608,7 +560,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "sum by (status) (mcpserver_condition_info{type=\"Accepted\", namespace=~\"$mcpserver_namespace\"})", @@ -623,7 +575,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -657,7 +609,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "sum by (status) (mcpserver_condition_info{type=\"Ready\", namespace=~\"$mcpserver_namespace\"})", @@ -672,7 +624,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -713,7 +665,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(rate(mcpserver_deployment_failures_total{namespace=~\"$mcpserver_namespace\"}[5m])) by (namespace)", @@ -728,7 +680,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -769,7 +721,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(rate(mcpserver_service_failures_total{namespace=~\"$mcpserver_namespace\"}[5m])) by (namespace)", @@ -784,7 +736,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -825,7 +777,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(rate(mcpserver_networkpolicy_failures_total{namespace=~\"$mcpserver_namespace\"}[5m])) by (namespace)", @@ -853,7 +805,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -887,13 +839,13 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "sum by (reason) (mcpserver_validation_failures_total{namespace=~\"$mcpserver_namespace\"})", "range": true, "refId": "A", - "legendFormat": "{{status}}" + "legendFormat": "{{reason}}" } ], "title": "Validation failures by reason", @@ -902,7 +854,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -943,7 +895,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "sum(rate(mcpserver_validation_failures_total{namespace=~\"$mcpserver_namespace\"}[5m])) by (namespace, reason)", @@ -971,7 +923,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -988,7 +940,7 @@ "mode": "none" } }, - "unit": "ops" + "unit": "short" }, "overrides": [] }, @@ -1012,10 +964,10 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", - "expr": "sum(rate(process_cpu_seconds_total[5m]))", + "expr": "sum(rate(process_cpu_seconds_total{namespace=~\"$operator_namespace\"}[5m]))", "range": true, "refId": "A", "legendFormat": "CPU" @@ -1027,7 +979,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -1044,7 +996,7 @@ "mode": "none" } }, - "unit": "ops" + "unit": "bytes" }, "overrides": [] }, @@ -1068,10 +1020,10 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", - "expr": "sum(process_resident_memory_bytes)", + "expr": "sum(process_resident_memory_bytes{namespace=~\"$operator_namespace\"})", "range": true, "refId": "A", "legendFormat": "memory" @@ -1083,7 +1035,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -1100,7 +1052,7 @@ "mode": "none" } }, - "unit": "ops" + "unit": "short" }, "overrides": [] }, @@ -1124,7 +1076,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", "expr": "controller_runtime_active_workers{controller=\"mcpserver\"}", @@ -1139,7 +1091,7 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "fieldConfig": { "defaults": { @@ -1180,10 +1132,10 @@ { "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, "editorMode": "code", - "expr": "sum(rate(rest_client_requests_total[5m])) by (code)", + "expr": "sum(rate(rest_client_requests_total{namespace=~\"$operator_namespace\"}[5m])) by (code)", "range": true, "refId": "A", "legendFormat": "{{code}}" @@ -1202,6 +1154,23 @@ ], "templating": { "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "prometheus" + }, + "hide": 0, + "includeAll": false, + "label": "Datasource", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "refresh": 1, + "regex": "", + "type": "datasource" + }, { "current": { "selected": true, @@ -1210,17 +1179,17 @@ }, "datasource": { "type": "prometheus", - "uid": "${DS_PROMETHEUS}" + "uid": "${datasource}" }, - "definition": "label_values(mcpserver_condition_info, namespace)", + "definition": "label_values(process_cpu_seconds_total, namespace)", "hide": 0, "includeAll": true, - "label": "MCPServer namespace", - "multi": true, - "name": "mcpserver_namespace", + "label": "Operator namespace", + "multi": false, + "name": "operator_namespace", "options": [], "query": { - "query": "label_values(mcpserver_condition_info, namespace)", + "query": "label_values(process_cpu_seconds_total, namespace)", "refId": "PrometheusVariableQuery" }, "refresh": 2, diff --git a/go.mod b/go.mod index 40891725..5230a5bb 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,8 @@ require ( github.com/onsi/ginkgo/v2 v2.31.0 github.com/onsi/gomega v1.42.0 github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/prometheus v0.303.0 + gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.36.2 k8s.io/apimachinery v0.36.2 k8s.io/client-go v0.36.2 @@ -27,6 +29,7 @@ require ( github.com/cenkalti/backoff/v5 v5.0.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/dennwc/varint v1.0.0 // indirect github.com/emicklei/go-restful/v3 v3.13.0 // indirect github.com/evanphx/json-patch/v5 v5.9.11 // indirect github.com/felixge/httpsnoop v1.1.0 // indirect @@ -57,6 +60,7 @@ require ( github.com/google/pprof v0.0.0-20260604005048-7023385849c0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect + github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -84,6 +88,7 @@ require ( go.opentelemetry.io/otel/sdk v1.44.0 // indirect go.opentelemetry.io/otel/trace v1.44.0 // indirect go.opentelemetry.io/proto/otlp v1.10.0 // indirect + go.uber.org/atomic v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.28.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect diff --git a/go.sum b/go.sum index ac66e49e..269a65fb 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,31 @@ cel.dev/expr v0.25.2 h1:K6j46C81hXtZQfuX60cVWQFBJahKSE2gfRbNuvr5bFs= cel.dev/expr v0.25.2/go.mod h1:hrXvqGP6G6gyx8UAHSHJ5RGk//1Oj5nXQ2NI02Nrsg4= +cloud.google.com/go/auth v0.15.0 h1:Ly0u4aA5vG/fsSsxu98qCQBemXtAtJf+95z9HK+cxps= +cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQNVXLZ8= +cloud.google.com/go/auth/oauth2adapt v0.2.7 h1:/Lc7xODdqcEw8IrZ9SvwnlLX6j9FHQM74z6cBk9Rw6M= +cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc= +cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= +cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4= +github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/Masterminds/semver/v3 v3.5.0 h1:kQceYJfbupGfZOKZQg0kou0DgAKhzDg2NZPAwZ/2OOE= github.com/Masterminds/semver/v3 v3.5.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b h1:mimo19zliBX/vSQ6PWWSL9lK8qwHozUj03+zLoEB8O0= +github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b/go.mod h1:fvzegU4vN3H1qMT+8wDmzjAcDONcgo2/SZ/TyfdUOFs= github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ= github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= +github.com/aws/aws-sdk-go v1.55.6 h1:cSg4pvZ3m8dgYcgqB97MrcdjUmZ1BeMYKUxMMB89IPk= +github.com/aws/aws-sdk-go v1.55.6/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3 h1:6df1vn4bBlDDo4tARvBm7l6KA9iVMnE3NWizDeWSrps= +github.com/bboreham/go-loser v0.0.0-20230920113527-fcc2c21820a3/go.mod h1:CIWtjkly68+yqLPbvwwR/fjNJA/idrtULjZWh2v1ys0= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -19,6 +39,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dennwc/varint v1.0.0 h1:kGNFFSSw8ToIy3obO/kKr8U9GZYUAxQEVuix4zfDWzE= +github.com/dennwc/varint v1.0.0/go.mod h1:hnItb35rvZvJrbTALZtY/iQfDs48JKRG1RPpgziApxA= github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch v0.5.2 h1:xVCHIVMUu1wtM/VkR9jVZ45N3FhZfYMMYGorLCR8P3k= @@ -86,6 +108,8 @@ github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63Y github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/cel-go v0.28.1 h1:YWIwi77J4xIsYUwAF/iIuS6haffzIHS8yWI8glSbLWM= github.com/google/cel-go v0.28.1/go.mod h1:X0bD6iVNR8pkROSOoHVdgTkzmRcosof7WQqCD6wcMc8= github.com/google/gnostic-models v0.7.1 h1:SisTfuFKJSKM5CPZkffwi6coztzzeYUhc3v4yxLWH8c= @@ -99,16 +123,28 @@ github.com/google/jsonschema-go v0.4.3 h1:/DBOLZTfDow7pe2GmaJNhltueGTtDKICi8V8p+ github.com/google/jsonschema-go v0.4.3/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= github.com/google/pprof v0.0.0-20260604005048-7023385849c0 h1:h1QTMDl6q9wDvDCJVpKQSjgleGFYnd2fOxmg2K+6BGE= github.com/google/pprof v0.0.0-20260604005048-7023385849c0/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.5 h1:VgzTY2jogw3xt39CusEnFJWm7rlsq5yL5q9XdLOuP5g= +github.com/googleapis/enterprise-certificate-proxy v0.3.5/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.14.1 h1:hb0FFeiPaQskmvakKu5EbCbpntQn48jyHuvrkurSS/Q= +github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc h1:GN2Lv3MGO7AS6PrRoT6yV5+wkrOpcszoIsO4+4ds248= +github.com/grafana/regexp v0.0.0-20240518133315-a468a5bfb3bc/go.mod h1:+JKpmjMGhpgPL+rXZ5nsZieVzvarn86asRlBg4uNGnk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk= github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= +github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -135,10 +171,17 @@ github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFd github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU= +github.com/oklog/ulid/v2 v2.1.0/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNsTT1QQ= github.com/onsi/ginkgo/v2 v2.31.0 h1:GtuJos5DFUV9EerYJo8RhYxosYNGvOdDE5haKq6Grfs= github.com/onsi/ginkgo/v2 v2.31.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= github.com/onsi/gomega v1.42.0 h1:CJby8u36xb7v34W78F8WKvqTQP7PCMIPB78IVDB73l4= github.com/onsi/gomega v1.42.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -152,6 +195,10 @@ github.com/prometheus/common v0.69.0 h1:OA85nJQS/T/MaYh/Q2CcgDKSGWqNIgrBDvDH85Cu github.com/prometheus/common v0.69.0/go.mod h1:ZzL3f6u94qUxh9p+tJTrF+FvBS1XXbbRAZCQkytAL0Y= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo= +github.com/prometheus/prometheus v0.303.0 h1:wsNNsbd4EycMCphYnTmNY9JASBVbp7NWwJna857cGpA= +github.com/prometheus/prometheus v0.303.0/go.mod h1:8PMRi+Fk1WzopMDeb0/6hbNs9nV6zgySkU/zds5Lu3o= +github.com/prometheus/sigv4 v0.1.2 h1:R7570f8AoM5YnTUPFm3mjZH5q2k4D+I/phCWvZ4PXG8= +github.com/prometheus/sigv4 v0.1.2/go.mod h1:GF9fwrvLgkQwDdQ5BXeV9XUSCH/IPNqzvAoaohfjqMU= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= @@ -204,6 +251,8 @@ go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/ go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g= go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk= +go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= +go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -214,6 +263,8 @@ go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= +golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M= golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ= @@ -238,6 +289,8 @@ gomodules.xyz/jsonpatch/v2 v2.5.0 h1:JELs8RLM12qJGXU4u/TO3V25KW8GreMKl9pdkk14RM0 gomodules.xyz/jsonpatch/v2 v2.5.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= +google.golang.org/api v0.224.0 h1:Ir4UPtDsNiwIOHdExr3fAj4xZ42QjK7uQte3lORLJwU= +google.golang.org/api v0.224.0/go.mod h1:3V39my2xAGkodXy0vEqcEtkqgw2GtrFL5WuBZlCTCOQ= google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d h1:xr2lwHI91bn3UiXcnyzRMQjp2LRiM8wEHzwUaE0YhTs= google.golang.org/genproto/googleapis/api v0.0.0-20260622175928-b703f567277d/go.mod h1:O0ZOWSrfWfJ+Z5HbwZ+wNtHsg/vk1k2C/w67eww8PfQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d h1:mpAgMyM9vQHxycBlDq50y1VHpfSfVwzXvrQKtYbXuUY= @@ -253,6 +306,8 @@ gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnf gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= k8s.io/api v0.36.2 h1:TF6YDLIzKfccK7cq9YpTcGX8TJmEkHVRv78DM51fRYY= diff --git a/hack/verify-grafana-dashboard.sh b/hack/verify-grafana-dashboard.sh index 63a72011..8c8f40e1 100755 --- a/hack/verify-grafana-dashboard.sh +++ b/hack/verify-grafana-dashboard.sh @@ -33,7 +33,7 @@ if [[ -n "${PROMETHEUS_URL:-}" ]]; then ) for q in "${queries[@]}"; do encoded="$(python3 -c "import urllib.parse; print(urllib.parse.quote('''${q}'''))")" - result="$(curl -sf "${PROMETHEUS_URL}/api/v1/query?query=${encoded}")" + result="$(curl -sf --connect-timeout 5 --max-time 15 "${PROMETHEUS_URL}/api/v1/query?query=${encoded}")" count="$(echo "${result}" | python3 -c "import json,sys; d=json.load(sys.stdin); print(len(d.get('data',{}).get('result',[])))")" echo " query=${q} series=${count}" if [[ "${count}" -eq 0 ]]; then diff --git a/site-src/operating/metrics.md b/site-src/operating/metrics.md index d4175871..04f0a7f2 100644 --- a/site-src/operating/metrics.md +++ b/site-src/operating/metrics.md @@ -151,26 +151,30 @@ The repository ships an example Grafana dashboard at [`config/grafana/mcp-lifecy - **Validation** — validation failures by reason - **Performance** — operator CPU/memory, active reconcile workers, and API server request rate -Panels use only metrics that the operator exports today (`mcpserver_*` and controller-runtime `controller_runtime_*` series). ConfigMap/Secret watch metrics from early design notes are **not** included because they are not implemented. +Panels use only metrics that the operator exports today: custom `mcpserver_*` series, controller-runtime `controller_runtime_*` series, and standard Go/process metrics (`process_cpu_seconds_total`, `process_resident_memory_bytes`, `rest_client_requests_total`). Ensure your Prometheus scrape config collects the controller-manager `/metrics` endpoint so these series are available. ConfigMap/Secret watch metrics from early design notes are **not** included because they are not implemented. + +Performance panels scope `process_*` and `rest_client_*` queries by operator namespace when Prometheus adds Kubernetes labels during scrape (for example via `ServiceMonitor`). If you scrape the raw `/metrics` endpoint without relabeling, select **All** for the **Operator namespace** dashboard variable. ### Import via Grafana UI 1. Open your Grafana instance and go to **Dashboards → New → Import**. 2. Upload [`config/grafana/mcp-lifecycle-operator.json`](https://github.com/kubernetes-sigs/mcp-lifecycle-operator/blob/main/config/grafana/mcp-lifecycle-operator.json), or paste the [raw GitHub URL](https://raw.githubusercontent.com/kubernetes-sigs/mcp-lifecycle-operator/main/config/grafana/mcp-lifecycle-operator.json). -3. Select your Prometheus data source when prompted (`DS_PROMETHEUS`). +3. Select your Prometheus data source when prompted (the dashboard uses a **Datasource** template variable). 4. Save the dashboard. This follows the same **raw JSON + manual import** pattern used by [node-feature-discovery](https://github.com/kubernetes-sigs/node-feature-discovery/blob/master/docs/deployment/metrics.md) and the [Kubebuilder Grafana plugin](https://book.kubebuilder.io/plugins/available/grafana-v1-alpha.html). ### Provision with Grafana sidecar (optional) -If you run Grafana with the [kiwigrid sidecar](https://github.com/grafana/helm-charts/tree/main/charts/grafana) (for example via [kube-prometheus-stack](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack)), apply the labeled ConfigMap: +If you run Grafana with the [kiwigrid sidecar](https://github.com/grafana/helm-charts/tree/main/charts/grafana) (for example via [kube-prometheus-stack](https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack)), apply the labeled ConfigMap from a checkout of this repository: ```bash +git clone https://github.com/kubernetes-sigs/mcp-lifecycle-operator.git +cd mcp-lifecycle-operator kubectl apply -k config/grafana/ ``` -The Kustomize overlay sets `grafana_dashboard: "1"` and `grafana_folder: MCP Lifecycle Operator`, matching conventions from [Strimzi](https://github.com/strimzi/strimzi-kafka-operator) and kube-prometheus-stack. +The Kustomize overlay sets `grafana_dashboard: "1"` and `grafana_folder: MCP Lifecycle Operator`, matching conventions from [Strimzi](https://github.com/strimzi/strimzi-kafka-operator) and kube-prometheus-stack. The `grafana_folder` annotation requires Grafana Helm values `sidecar.dashboards.folderAnnotation: grafana_folder` and `sidecar.dashboards.provider.foldersFromFilesStructure: true`; otherwise dashboards import successfully but may appear in the default folder. ## Next steps From e8a2db39965fd7f46e6556ba3df0ee91f34094bb Mon Sep 17 00:00:00 2001 From: Adarsh Kumar Yadav Date: Wed, 15 Jul 2026 12:05:56 +0530 Subject: [PATCH 3/3] Fix CI lint and test failures for Grafana dashboard validation. Use strings.SplitSeq for modernize lint compliance and ensure kustomize is installed before verify-grafana-dashboard runs in CI. --- Makefile | 2 +- config/grafana/dashboard_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 328dcaac..a366f90a 100644 --- a/Makefile +++ b/Makefile @@ -75,7 +75,7 @@ COVER_PROFILE ?= cover.out COVER_OUTPUT_DIR ?= out .PHONY: verify-grafana-dashboard -verify-grafana-dashboard: ## Validate Grafana dashboard JSON and PromQL references. +verify-grafana-dashboard: kustomize ## Validate Grafana dashboard JSON and PromQL references. chmod +x hack/verify-grafana-dashboard.sh ./hack/verify-grafana-dashboard.sh diff --git a/config/grafana/dashboard_test.go b/config/grafana/dashboard_test.go index fe62b362..5ba9c427 100644 --- a/config/grafana/dashboard_test.go +++ b/config/grafana/dashboard_test.go @@ -218,7 +218,7 @@ func metricSelectorsFromExpr(expr string) ([]string, error) { func splitYAMLDocuments(raw string) []string { var docs []string - for _, part := range strings.Split(raw, "\n---") { + for part := range strings.SplitSeq(raw, "\n---") { part = strings.TrimSpace(part) if part != "" { docs = append(docs, part)