You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@dubbo.apache.org by zh...@apache.org on 2026/04/30 07:39:39 UTC

(dubbo-kubernetes) branch master updated: Added observability and e2e testing (#899)

This is an automated email from the ASF dual-hosted git repository.

mfordjody pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/dubbo-kubernetes.git


The following commit(s) were added to refs/heads/master by this push:
     new f7346507 Added observability and e2e testing (#899)
f7346507 is described below

commit f7346507f3a0145f9f8fa7ce8657b3020becbacc
Author: Joe Zhong <11...@qq.com>
AuthorDate: Thu Apr 30 15:39:34 2026 +0800

    Added observability and e2e testing (#899)
---
 .../charts/dubbod/templates/service.yaml           |   7 +
 dubbooperator/pkg/render/manifest_test.go          |  49 +-
 pkg/log/log.go                                     |  10 +
 pkg/log/log_test.go                                |  33 +
 pkg/monitoring/monitoring.go                       |  36 +-
 .../monitoring/monitoring_test.go                  |  37 +-
 .../addons/00-namespace.yaml                       |  39 +-
 samples/addons/README.md                           |  38 +
 samples/addons/grafana.yaml                        | 931 +++++++++++++++++++++
 samples/addons/prometheus.yaml                     | 132 +++
 tests/e2e/dubbod-observability.sh                  | 100 +++
 11 files changed, 1347 insertions(+), 65 deletions(-)

diff --git a/dubboinstaller/charts/dubbod/templates/service.yaml b/dubboinstaller/charts/dubbod/templates/service.yaml
index f462d01f..141ec006 100644
--- a/dubboinstaller/charts/dubbod/templates/service.yaml
+++ b/dubboinstaller/charts/dubbod/templates/service.yaml
@@ -20,8 +20,15 @@ metadata:
   namespace: dubbo-system
   labels:
     app: dubbod
+  annotations:
+    prometheus.io/path: /metrics
+    prometheus.io/port: "8080"
+    prometheus.io/scrape: "true"
 spec:
   ports:
+    - port: 8080
+      name: http-monitoring
+      protocol: TCP
     - port: 26010
       name: grpc-xds
       protocol: TCP
diff --git a/dubbooperator/pkg/render/manifest_test.go b/dubbooperator/pkg/render/manifest_test.go
index 3d5f4bf3..e988e151 100644
--- a/dubbooperator/pkg/render/manifest_test.go
+++ b/dubbooperator/pkg/render/manifest_test.go
@@ -15,7 +15,11 @@
 
 package render
 
-import "testing"
+import (
+	"testing"
+
+	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
+)
 
 func TestGenerateManifestUsesEmbeddedInstallerAssets(t *testing.T) {
 	manifests, _, err := GenerateManifest(nil, nil, nil, nil)
@@ -26,3 +30,46 @@ func TestGenerateManifestUsesEmbeddedInstallerAssets(t *testing.T) {
 		t.Fatal("GenerateManifest() returned zero manifest sets")
 	}
 }
+
+func TestGenerateManifestExposesDubbodPrometheusScrapeEndpoint(t *testing.T) {
+	manifests, _, err := GenerateManifest(nil, nil, nil, nil)
+	if err != nil {
+		t.Fatalf("GenerateManifest() error = %v", err)
+	}
+
+	for _, set := range manifests {
+		for _, m := range set.Manifests {
+			if m.GetKind() != "Service" || m.GetName() != "dubbod" || m.GetNamespace() != "dubbo-system" {
+				continue
+			}
+			annotations := m.GetAnnotations()
+			if annotations["prometheus.io/scrape"] != "true" {
+				t.Fatalf("prometheus.io/scrape = %q, want true", annotations["prometheus.io/scrape"])
+			}
+			if annotations["prometheus.io/path"] != "/metrics" {
+				t.Fatalf("prometheus.io/path = %q, want /metrics", annotations["prometheus.io/path"])
+			}
+			if annotations["prometheus.io/port"] != "8080" {
+				t.Fatalf("prometheus.io/port = %q, want 8080", annotations["prometheus.io/port"])
+			}
+
+			ports, ok, err := unstructured.NestedSlice(m.Object, "spec", "ports")
+			if err != nil || !ok {
+				t.Fatalf("spec.ports missing: ok=%v err=%v", ok, err)
+			}
+			for _, port := range ports {
+				p, ok := port.(map[string]interface{})
+				if !ok {
+					continue
+				}
+				name, _, _ := unstructured.NestedString(p, "name")
+				number, _, _ := unstructured.NestedInt64(p, "port")
+				if name == "http-monitoring" && number == 8080 {
+					return
+				}
+			}
+			t.Fatal("dubbod Service missing http-monitoring port 8080")
+		}
+	}
+	t.Fatal("dubbod Service not rendered")
+}
diff --git a/pkg/log/log.go b/pkg/log/log.go
index 549874c0..7aa47a9b 100644
--- a/pkg/log/log.go
+++ b/pkg/log/log.go
@@ -24,6 +24,8 @@ import (
 	"strings"
 	"sync"
 	"time"
+
+	"github.com/apache/dubbo-kubernetes/pkg/monitoring"
 )
 
 // Level represents the logging level
@@ -147,6 +149,13 @@ var (
 	defaultScope      = "default"
 	defaultLogger     *Logger
 	defaultLoggerOnce sync.Once
+	levelTag          = monitoring.CreateLabel("level")
+	scopeTag          = monitoring.CreateLabel("scope")
+	logMessages       = monitoring.NewSum(
+		"dubbod_log_messages_total",
+		"Total log messages emitted by level and scope.",
+		monitoring.WithLabels("level", "scope"),
+	)
 )
 
 // RegisterScope creates and registers a new logging scope
@@ -257,6 +266,7 @@ func (l *Logger) logMessage(level Level, msg string, preserveNewlines bool) {
 
 	levelName := levelNames[level]
 	scopeName := l.scope.Name()
+	logMessages.With(levelTag.Value(levelName), scopeTag.Value(scopeName)).Increment()
 
 	// Check deduplication
 	dedup := GetDeduplicator()
diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go
index 7c52db0f..470a6ac6 100644
--- a/pkg/log/log_test.go
+++ b/pkg/log/log_test.go
@@ -21,6 +21,8 @@ import (
 	"encoding/json"
 	"strings"
 	"testing"
+
+	"github.com/apache/dubbo-kubernetes/pkg/monitoring"
 )
 
 func TestInfoJSONWritesIndentedJSONBlock(t *testing.T) {
@@ -55,3 +57,34 @@ func TestInfoJSONWritesIndentedJSONBlock(t *testing.T) {
 		t.Fatalf("pushVersion = %#v, want v1", parsed["pushVersion"])
 	}
 }
+
+func TestLogMessagesMetricRecordsLevelAndScope(t *testing.T) {
+	var out bytes.Buffer
+	logger := RegisterScope("log-metric-test", "log metric test")
+	logger.Scope().SetOutput(&out)
+
+	logger.Warn("metric event")
+
+	families, err := monitoring.GetRegistry().Gather()
+	if err != nil {
+		t.Fatalf("gather metrics: %v", err)
+	}
+	for _, family := range families {
+		if family.GetName() != "dubbod_log_messages_total" {
+			continue
+		}
+		for _, metric := range family.GetMetric() {
+			labels := map[string]string{}
+			for _, label := range metric.GetLabel() {
+				labels[label.GetName()] = label.GetValue()
+			}
+			if labels["level"] == "warn" && labels["scope"] == "log-metric-test" {
+				if metric.GetGauge().GetValue() < 1 {
+					t.Fatalf("dubbod_log_messages_total = %v, want >= 1", metric.GetGauge().GetValue())
+				}
+				return
+			}
+		}
+	}
+	t.Fatal("dubbod_log_messages_total with level=warn scope=log-metric-test not found")
+}
diff --git a/pkg/monitoring/monitoring.go b/pkg/monitoring/monitoring.go
index 82b5e0e9..35603243 100644
--- a/pkg/monitoring/monitoring.go
+++ b/pkg/monitoring/monitoring.go
@@ -334,6 +334,11 @@ type distribution struct {
 }
 
 func newDistribution(name, description string, bounds []float64, opts ...Options) Metric {
+	o := &options{}
+	for _, opt := range opts {
+		opt(o)
+	}
+
 	d := &distribution{
 		name:        name,
 		description: description,
@@ -345,13 +350,19 @@ func newDistribution(name, description string, bounds []float64, opts ...Options
 			Name:    name,
 			Help:    description,
 			Buckets: bounds,
-		}, []string{})
+		}, o.labels)
 
 		registryLock.Lock()
 		registry.MustRegister(d.histogram)
 		registryLock.Unlock()
 	})
 
+	if len(o.labels) > 0 {
+		return &distributionMetric{
+			parent: d,
+		}
+	}
+
 	return &distributionMetric{
 		parent:    d,
 		histogram: d.histogram.WithLabelValues(),
@@ -377,6 +388,9 @@ func (dm *distributionMetric) Name() string {
 }
 
 func (dm *distributionMetric) Record(value float64) {
+	if dm.histogram == nil {
+		return
+	}
 	dm.histogram.Observe(value)
 }
 
@@ -389,29 +403,11 @@ func (dm *distributionMetric) With(labelValues ...LabelValue) Metric {
 		return dm
 	}
 
-	// Create new histogram vec with labels if needed
-	labelNames := make([]string, len(labelValues))
 	labels := make(prometheus.Labels)
-	for i, lv := range labelValues {
-		labelNames[i] = lv.Name
+	for _, lv := range labelValues {
 		labels[lv.Name] = lv.Value
 	}
 
-	// Check if we need to recreate the histogram with labels
-	if dm.labels == nil {
-		newHist := prometheus.NewHistogramVec(prometheus.HistogramOpts{
-			Name:    dm.parent.name,
-			Help:    dm.parent.description,
-			Buckets: dm.parent.bounds,
-		}, labelNames)
-
-		registryLock.Lock()
-		registry.MustRegister(newHist)
-		registryLock.Unlock()
-
-		dm.parent.histogram = newHist
-	}
-
 	return &distributionMetric{
 		parent:    dm.parent,
 		histogram: dm.parent.histogram.With(labels),
diff --git a/dubbooperator/pkg/render/manifest_test.go b/pkg/monitoring/monitoring_test.go
similarity index 51%
copy from dubbooperator/pkg/render/manifest_test.go
copy to pkg/monitoring/monitoring_test.go
index 3d5f4bf3..91bf05d2 100644
--- a/dubbooperator/pkg/render/manifest_test.go
+++ b/pkg/monitoring/monitoring_test.go
@@ -1,3 +1,4 @@
+//
 // Licensed to the Apache Software Foundation (ASF) under one or more
 // contributor license agreements.  See the NOTICE file distributed with
 // this work for additional information regarding copyright ownership.
@@ -13,16 +14,38 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package render
+package monitoring
+
+import (
+	"fmt"
+	"testing"
+	"time"
+)
+
+func TestDistributionWithLabelsRegistersOnce(t *testing.T) {
+	phaseTag := CreateLabel("phase")
+	name := fmt.Sprintf("test_distribution_with_labels_seconds_%d", time.Now().UnixNano())
+	metric := NewDistribution(
+		name,
+		"Test labeled distribution registration.",
+		[]float64{0.1, 1},
+		WithLabels("phase"),
+	)
 
-import "testing"
+	metric.With(phaseTag.Value("push")).Record(0.2)
+	metric.With(phaseTag.Value("send")).Record(0.4)
 
-func TestGenerateManifestUsesEmbeddedInstallerAssets(t *testing.T) {
-	manifests, _, err := GenerateManifest(nil, nil, nil, nil)
+	families, err := GetRegistry().Gather()
 	if err != nil {
-		t.Fatalf("GenerateManifest() error = %v", err)
+		t.Fatalf("gather metrics: %v", err)
 	}
-	if len(manifests) == 0 {
-		t.Fatal("GenerateManifest() returned zero manifest sets")
+	for _, family := range families {
+		if family.GetName() == name {
+			if got := len(family.GetMetric()); got != 2 {
+				t.Fatalf("metric series = %d, want 2", got)
+			}
+			return
+		}
 	}
+	t.Fatalf("%s not gathered", name)
 }
diff --git a/dubboinstaller/charts/dubbod/templates/service.yaml b/samples/addons/00-namespace.yaml
similarity index 57%
copy from dubboinstaller/charts/dubbod/templates/service.yaml
copy to samples/addons/00-namespace.yaml
index f462d01f..42e674c6 100644
--- a/dubboinstaller/charts/dubbod/templates/service.yaml
+++ b/samples/addons/00-namespace.yaml
@@ -14,41 +14,6 @@
 # limitations under the License.
 
 apiVersion: v1
-kind: Service
+kind: Namespace
 metadata:
-  name: dubbod
-  namespace: dubbo-system
-  labels:
-    app: dubbod
-spec:
-  ports:
-    - port: 26010
-      name: grpc-xds
-      protocol: TCP
-    - port: 26012
-      name: https-dns
-      protocol: TCP
-    - port: 443
-      name: https-webhook
-      targetPort: 26017
-      protocol: TCP
-  selector:
-    app: dubbod
----
-apiVersion: v1
-kind: Service
-metadata:
-  name: dubbod-gui
-  namespace: dubbo-system
-  labels:
-    app: dubbod
-spec:
-  type: NodePort
-  ports:
-    - port: 26080
-      name: gui
-      targetPort: 26080
-      nodePort: 30080
-      protocol: TCP
-  selector:
-    app: dubbod
+  name: dubbo-system
diff --git a/samples/addons/README.md b/samples/addons/README.md
new file mode 100644
index 00000000..c1538a72
--- /dev/null
+++ b/samples/addons/README.md
@@ -0,0 +1,38 @@
+# Dubbo 可观测性 Addons
+
+这个目录提供开箱即用的 Prometheus 和 Grafana 部署,用于观察 `dubbo-system/dubbod` 的日志信号、监控指标和控制面追踪耗时。
+
+## 安装
+
+```bash
+kubectl apply -f samples/addons
+kubectl -n dubbo-system rollout status deploy/prometheus --timeout=180s
+kubectl -n dubbo-system rollout status deploy/grafana --timeout=180s
+```
+
+## 打开 Grafana
+
+```bash
+kubectl -n dubbo-system port-forward svc/grafana 3000:3000
+```
+
+浏览器打开 `http://127.0.0.1:3000`,进入 `Dubbo / Dubbo Control Plane Observability` 仪表盘。
+
+## 验证 Prometheus
+
+```bash
+kubectl -n dubbo-system port-forward svc/prometheus 9090:9090
+curl 'http://127.0.0.1:9090/api/v1/query?query=dubbod_uptime_seconds'
+```
+
+## 覆盖内容
+
+- 日志:`dubbod_log_messages_total` 按 `level` 和 `scope` 统计日志速率,原始日志仍通过 `kubectl -n dubbo-system logs deploy/dubbod` 查看。
+- 监控:Prometheus 抓取 `dubbod.dubbo-system.svc:8080/metrics`,Grafana 内置控制面总览面板。
+- 追踪:通过 `dubbod_xds_push_time`、`dubbod_xds_send_time`、`dubbod_proxy_queue_time` 等直方图展示 xDS 推送链路耗时。
+
+## 清理
+
+```bash
+kubectl delete -f samples/addons --ignore-not-found=true
+```
diff --git a/samples/addons/grafana.yaml b/samples/addons/grafana.yaml
new file mode 100644
index 00000000..463c0414
--- /dev/null
+++ b/samples/addons/grafana.yaml
@@ -0,0 +1,931 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: grafana
+  namespace: dubbo-system
+  labels:
+    app.kubernetes.io/name: grafana
+    app.kubernetes.io/part-of: dubbo-observability
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: grafana
+  namespace: dubbo-system
+  labels:
+    app.kubernetes.io/name: grafana
+    app.kubernetes.io/part-of: dubbo-observability
+data:
+  grafana.ini: |
+    [analytics]
+    check_for_updates = false
+    [log]
+    mode = console
+    [paths]
+    data = /var/lib/grafana
+    logs = /var/log/grafana
+    plugins = /var/lib/grafana/plugins
+    provisioning = /etc/grafana/provisioning
+  datasources.yaml: |
+    apiVersion: 1
+    datasources:
+    - name: Prometheus
+      uid: prometheus
+      type: prometheus
+      access: proxy
+      orgId: 1
+      url: http://prometheus.dubbo-system.svc:9090
+      isDefault: true
+      editable: true
+      jsonData:
+        timeInterval: 15s
+  dashboardproviders.yaml: |
+    apiVersion: 1
+    providers:
+    - name: dubbo
+      orgId: 1
+      folder: Dubbo
+      type: file
+      disableDeletion: false
+      editable: true
+      options:
+        path: /var/lib/grafana/dashboards/dubbo
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: dubbod-grafana-dashboards
+  namespace: dubbo-system
+  labels:
+    app.kubernetes.io/name: grafana
+    app.kubernetes.io/part-of: dubbo-observability
+data:
+  dubbod-observability.json: |
+    {
+      "annotations": {
+        "list": []
+      },
+      "editable": true,
+      "fiscalYearStartMonth": 0,
+      "graphTooltip": 0,
+      "id": null,
+      "links": [],
+      "liveNow": false,
+      "panels": [
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "decimals": 0,
+              "unit": "s"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 6,
+            "x": 0,
+            "y": 0
+          },
+          "id": 1,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "area",
+            "justifyMode": "auto",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "fields": "",
+              "values": false
+            },
+            "textMode": "auto"
+          },
+          "targets": [
+            {
+              "expr": "max(dubbod_uptime_seconds)",
+              "refId": "A"
+            }
+          ],
+          "title": "Uptime",
+          "type": "stat"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "decimals": 0
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 6,
+            "x": 6,
+            "y": 0
+          },
+          "id": 2,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "area",
+            "justifyMode": "auto",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "fields": "",
+              "values": false
+            },
+            "textMode": "auto"
+          },
+          "targets": [
+            {
+              "expr": "sum(dubbod_xds)",
+              "refId": "A"
+            }
+          ],
+          "title": "XDS Clients",
+          "type": "stat"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "decimals": 0
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 6,
+            "x": 12,
+            "y": 0
+          },
+          "id": 3,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "area",
+            "justifyMode": "auto",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "fields": "",
+              "values": false
+            },
+            "textMode": "auto"
+          },
+          "targets": [
+            {
+              "expr": "max(dubbod_services)",
+              "refId": "A"
+            }
+          ],
+          "title": "Services",
+          "type": "stat"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "decimals": 3,
+              "unit": "ops"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 4,
+            "w": 6,
+            "x": 18,
+            "y": 0
+          },
+          "id": 4,
+          "options": {
+            "colorMode": "value",
+            "graphMode": "area",
+            "justifyMode": "auto",
+            "orientation": "auto",
+            "reduceOptions": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "fields": "",
+              "values": false
+            },
+            "textMode": "auto"
+          },
+          "targets": [
+            {
+              "expr": "sum(rate(dubbod_log_messages_total{level=~\"error|warn\"}[5m]))",
+              "refId": "A"
+            }
+          ],
+          "title": "Warn/Error Log Rate",
+          "type": "stat"
+        },
+        {
+          "collapsed": false,
+          "gridPos": {
+            "h": 1,
+            "w": 24,
+            "x": 0,
+            "y": 4
+          },
+          "id": 5,
+          "panels": [],
+          "title": "Logs",
+          "type": "row"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "custom": {
+                "drawStyle": "line",
+                "fillOpacity": 10,
+                "lineWidth": 1,
+                "showPoints": "never"
+              },
+              "unit": "ops"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 0,
+            "y": 5
+          },
+          "id": 6,
+          "options": {
+            "legend": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "displayMode": "table",
+              "placement": "bottom",
+              "showLegend": true
+            },
+            "tooltip": {
+              "mode": "multi",
+              "sort": "none"
+            }
+          },
+          "targets": [
+            {
+              "expr": "sum by (level) (rate(dubbod_log_messages_total[5m]))",
+              "legendFormat": "{{level}}",
+              "refId": "A"
+            }
+          ],
+          "title": "Log Rate By Level",
+          "type": "timeseries"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "custom": {
+                "drawStyle": "line",
+                "fillOpacity": 10,
+                "lineWidth": 1,
+                "showPoints": "never"
+              },
+              "unit": "ops"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 12,
+            "y": 5
+          },
+          "id": 7,
+          "options": {
+            "legend": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "displayMode": "table",
+              "placement": "bottom",
+              "showLegend": true
+            },
+            "tooltip": {
+              "mode": "multi",
+              "sort": "none"
+            }
+          },
+          "targets": [
+            {
+              "expr": "sum by (scope) (rate(dubbod_log_messages_total{level=~\"warn|error\"}[5m]))",
+              "legendFormat": "{{scope}}",
+              "refId": "A"
+            }
+          ],
+          "title": "Warn/Error Logs By Scope",
+          "type": "timeseries"
+        },
+        {
+          "collapsed": false,
+          "gridPos": {
+            "h": 1,
+            "w": 24,
+            "x": 0,
+            "y": 13
+          },
+          "id": 8,
+          "panels": [],
+          "title": "Monitoring",
+          "type": "row"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "custom": {
+                "drawStyle": "line",
+                "fillOpacity": 10,
+                "lineWidth": 1,
+                "showPoints": "never"
+              },
+              "unit": "ops"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 8,
+            "x": 0,
+            "y": 14
+          },
+          "id": 9,
+          "options": {
+            "legend": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "displayMode": "table",
+              "placement": "bottom",
+              "showLegend": true
+            },
+            "tooltip": {
+              "mode": "multi",
+              "sort": "none"
+            }
+          },
+          "targets": [
+            {
+              "expr": "sum by (type) (rate(dubbod_xds_pushes[5m]))",
+              "legendFormat": "{{type}}",
+              "refId": "A"
+            }
+          ],
+          "title": "XDS Push Rate",
+          "type": "timeseries"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "custom": {
+                "drawStyle": "line",
+                "fillOpacity": 10,
+                "lineWidth": 1,
+                "showPoints": "never"
+              },
+              "unit": "ops"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 8,
+            "x": 8,
+            "y": 14
+          },
+          "id": 10,
+          "options": {
+            "legend": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "displayMode": "table",
+              "placement": "bottom",
+              "showLegend": true
+            },
+            "tooltip": {
+              "mode": "multi",
+              "sort": "none"
+            }
+          },
+          "targets": [
+            {
+              "expr": "sum by (type) (rate(dubbod_push_triggers[5m]))",
+              "legendFormat": "{{type}}",
+              "refId": "A"
+            }
+          ],
+          "title": "Push Trigger Rate",
+          "type": "timeseries"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "custom": {
+                "drawStyle": "line",
+                "fillOpacity": 10,
+                "lineWidth": 1,
+                "showPoints": "never"
+              },
+              "unit": "ops"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 8,
+            "x": 16,
+            "y": 14
+          },
+          "id": 11,
+          "options": {
+            "legend": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "displayMode": "table",
+              "placement": "bottom",
+              "showLegend": true
+            },
+            "tooltip": {
+              "mode": "multi",
+              "sort": "none"
+            }
+          },
+          "targets": [
+            {
+              "expr": "sum by (type) (rate(dubbod_inbound_updates[5m]))",
+              "legendFormat": "{{type}}",
+              "refId": "A"
+            }
+          ],
+          "title": "Inbound Updates",
+          "type": "timeseries"
+        },
+        {
+          "collapsed": false,
+          "gridPos": {
+            "h": 1,
+            "w": 24,
+            "x": 0,
+            "y": 22
+          },
+          "id": 12,
+          "panels": [],
+          "title": "Tracing",
+          "type": "row"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "custom": {
+                "drawStyle": "line",
+                "fillOpacity": 10,
+                "lineWidth": 1,
+                "showPoints": "never"
+              },
+              "unit": "s"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 8,
+            "x": 0,
+            "y": 23
+          },
+          "id": 13,
+          "options": {
+            "legend": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "displayMode": "table",
+              "placement": "bottom",
+              "showLegend": true
+            },
+            "tooltip": {
+              "mode": "multi",
+              "sort": "none"
+            }
+          },
+          "targets": [
+            {
+              "expr": "histogram_quantile(0.99, sum by (le, type) (rate(dubbod_xds_push_time_bucket[5m])))",
+              "legendFormat": "{{type}}",
+              "refId": "A"
+            }
+          ],
+          "title": "XDS Push P99",
+          "type": "timeseries"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "custom": {
+                "drawStyle": "line",
+                "fillOpacity": 10,
+                "lineWidth": 1,
+                "showPoints": "never"
+              },
+              "unit": "s"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 8,
+            "x": 8,
+            "y": 23
+          },
+          "id": 14,
+          "options": {
+            "legend": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "displayMode": "table",
+              "placement": "bottom",
+              "showLegend": true
+            },
+            "tooltip": {
+              "mode": "multi",
+              "sort": "none"
+            }
+          },
+          "targets": [
+            {
+              "expr": "histogram_quantile(0.99, sum by (le) (rate(dubbod_xds_send_time_bucket[5m])))",
+              "legendFormat": "send",
+              "refId": "A"
+            },
+            {
+              "expr": "histogram_quantile(0.99, sum by (le) (rate(dubbod_proxy_queue_time_bucket[5m])))",
+              "legendFormat": "queue",
+              "refId": "B"
+            }
+          ],
+          "title": "Send And Queue P99",
+          "type": "timeseries"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "custom": {
+                "drawStyle": "line",
+                "fillOpacity": 10,
+                "lineWidth": 1,
+                "showPoints": "never"
+              },
+              "unit": "s"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 8,
+            "x": 16,
+            "y": 23
+          },
+          "id": 15,
+          "options": {
+            "legend": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "displayMode": "table",
+              "placement": "bottom",
+              "showLegend": true
+            },
+            "tooltip": {
+              "mode": "multi",
+              "sort": "none"
+            }
+          },
+          "targets": [
+            {
+              "expr": "histogram_quantile(0.99, sum by (le) (rate(dubbod_debounce_time_bucket[5m])))",
+              "legendFormat": "debounce",
+              "refId": "A"
+            },
+            {
+              "expr": "histogram_quantile(0.99, sum by (le) (rate(dubbod_pushcontext_init_seconds_bucket[5m])))",
+              "legendFormat": "push context",
+              "refId": "B"
+            }
+          ],
+          "title": "Control Plane Stage P99",
+          "type": "timeseries"
+        },
+        {
+          "collapsed": false,
+          "gridPos": {
+            "h": 1,
+            "w": 24,
+            "x": 0,
+            "y": 31
+          },
+          "id": 16,
+          "panels": [],
+          "title": "Errors",
+          "type": "row"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "custom": {
+                "drawStyle": "line",
+                "fillOpacity": 10,
+                "lineWidth": 1,
+                "showPoints": "never"
+              },
+              "unit": "ops"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 0,
+            "y": 32
+          },
+          "id": 17,
+          "options": {
+            "legend": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "displayMode": "table",
+              "placement": "bottom",
+              "showLegend": true
+            },
+            "tooltip": {
+              "mode": "multi",
+              "sort": "none"
+            }
+          },
+          "targets": [
+            {
+              "expr": "sum(rate(dubbod_total_xds_internal_errors[5m]))",
+              "legendFormat": "xds internal",
+              "refId": "A"
+            },
+            {
+              "expr": "sum(rate(dubbod_xds_write_timeout[5m]))",
+              "legendFormat": "write timeout",
+              "refId": "B"
+            },
+            {
+              "expr": "sum(rate(dubbod_sds_certificate_errors_total[5m]))",
+              "legendFormat": "sds certificate",
+              "refId": "C"
+            }
+          ],
+          "title": "Error Rate",
+          "type": "timeseries"
+        },
+        {
+          "datasource": {
+            "type": "prometheus",
+            "uid": "prometheus"
+          },
+          "fieldConfig": {
+            "defaults": {
+              "custom": {
+                "drawStyle": "line",
+                "fillOpacity": 10,
+                "lineWidth": 1,
+                "showPoints": "never"
+              },
+              "unit": "ops"
+            },
+            "overrides": []
+          },
+          "gridPos": {
+            "h": 8,
+            "w": 12,
+            "x": 12,
+            "y": 32
+          },
+          "id": 18,
+          "options": {
+            "legend": {
+              "calcs": [
+                "lastNotNull"
+              ],
+              "displayMode": "table",
+              "placement": "bottom",
+              "showLegend": true
+            },
+            "tooltip": {
+              "mode": "multi",
+              "sort": "none"
+            }
+          },
+          "targets": [
+            {
+              "expr": "sum by (type) (rate(dubbod_total_xds_rejects[5m]))",
+              "legendFormat": "{{type}}",
+              "refId": "A"
+            }
+          ],
+          "title": "XDS Reject Rate",
+          "type": "timeseries"
+        }
+      ],
+      "refresh": "15s",
+      "schemaVersion": 39,
+      "tags": [
+        "dubbo",
+        "dubbod",
+        "observability"
+      ],
+      "templating": {
+        "list": []
+      },
+      "time": {
+        "from": "now-30m",
+        "to": "now"
+      },
+      "timezone": "browser",
+      "title": "Dubbo Control Plane Observability",
+      "uid": "dubbod-observability",
+      "version": 1,
+      "weekStart": ""
+    }
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: grafana
+  namespace: dubbo-system
+  labels:
+    app.kubernetes.io/name: grafana
+    app.kubernetes.io/part-of: dubbo-observability
+spec:
+  type: ClusterIP
+  ports:
+  - name: http
+    port: 3000
+    protocol: TCP
+    targetPort: 3000
+  selector:
+    app.kubernetes.io/name: grafana
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: grafana
+  namespace: dubbo-system
+  labels:
+    app.kubernetes.io/name: grafana
+    app.kubernetes.io/part-of: dubbo-observability
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: grafana
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/name: grafana
+        app.kubernetes.io/part-of: dubbo-observability
+    spec:
+      serviceAccountName: grafana
+      containers:
+      - name: grafana
+        image: docker.io/grafana/grafana:12.0.1
+        imagePullPolicy: IfNotPresent
+        ports:
+        - name: http
+          containerPort: 3000
+          protocol: TCP
+        env:
+        - name: GF_AUTH_ANONYMOUS_ENABLED
+          value: "true"
+        - name: GF_AUTH_ANONYMOUS_ORG_ROLE
+          value: Admin
+        - name: GF_AUTH_BASIC_ENABLED
+          value: "false"
+        - name: GF_SECURITY_ADMIN_USER
+          value: admin
+        - name: GF_SECURITY_ADMIN_PASSWORD
+          value: admin
+        readinessProbe:
+          httpGet:
+            path: /api/health
+            port: 3000
+          initialDelaySeconds: 10
+          periodSeconds: 10
+        livenessProbe:
+          httpGet:
+            path: /api/health
+            port: 3000
+          initialDelaySeconds: 60
+          periodSeconds: 15
+        resources:
+          requests:
+            cpu: 100m
+            memory: 128Mi
+          limits:
+            memory: 512Mi
+        volumeMounts:
+        - name: config
+          mountPath: /etc/grafana/grafana.ini
+          subPath: grafana.ini
+          readOnly: true
+        - name: config
+          mountPath: /etc/grafana/provisioning/datasources/datasources.yaml
+          subPath: datasources.yaml
+          readOnly: true
+        - name: config
+          mountPath: /etc/grafana/provisioning/dashboards/dashboardproviders.yaml
+          subPath: dashboardproviders.yaml
+          readOnly: true
+        - name: dashboards
+          mountPath: /var/lib/grafana/dashboards/dubbo
+          readOnly: true
+        - name: storage
+          mountPath: /var/lib/grafana
+      volumes:
+      - name: config
+        configMap:
+          name: grafana
+      - name: dashboards
+        configMap:
+          name: dubbod-grafana-dashboards
+      - name: storage
+        emptyDir: {}
diff --git a/samples/addons/prometheus.yaml b/samples/addons/prometheus.yaml
new file mode 100644
index 00000000..7d46e93f
--- /dev/null
+++ b/samples/addons/prometheus.yaml
@@ -0,0 +1,132 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+  name: prometheus
+  namespace: dubbo-system
+  labels:
+    app.kubernetes.io/name: prometheus
+    app.kubernetes.io/part-of: dubbo-observability
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: prometheus
+  namespace: dubbo-system
+  labels:
+    app.kubernetes.io/name: prometheus
+    app.kubernetes.io/part-of: dubbo-observability
+data:
+  prometheus.yml: |
+    global:
+      scrape_interval: 15s
+      evaluation_interval: 15s
+
+    scrape_configs:
+    - job_name: prometheus
+      static_configs:
+      - targets:
+        - localhost:9090
+
+    - job_name: dubbod
+      metrics_path: /metrics
+      static_configs:
+      - targets:
+        - dubbod.dubbo-system.svc:8080
+        labels:
+          app: dubbod
+          namespace: dubbo-system
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: prometheus
+  namespace: dubbo-system
+  labels:
+    app.kubernetes.io/name: prometheus
+    app.kubernetes.io/part-of: dubbo-observability
+spec:
+  ports:
+  - name: http
+    port: 9090
+    protocol: TCP
+    targetPort: 9090
+  selector:
+    app.kubernetes.io/name: prometheus
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: prometheus
+  namespace: dubbo-system
+  labels:
+    app.kubernetes.io/name: prometheus
+    app.kubernetes.io/part-of: dubbo-observability
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app.kubernetes.io/name: prometheus
+  template:
+    metadata:
+      labels:
+        app.kubernetes.io/name: prometheus
+        app.kubernetes.io/part-of: dubbo-observability
+    spec:
+      serviceAccountName: prometheus
+      containers:
+      - name: prometheus
+        image: prom/prometheus:v3.5.0
+        imagePullPolicy: IfNotPresent
+        args:
+        - --config.file=/etc/prometheus/prometheus.yml
+        - --storage.tsdb.path=/prometheus
+        - --web.enable-lifecycle
+        ports:
+        - name: http
+          containerPort: 9090
+          protocol: TCP
+        readinessProbe:
+          httpGet:
+            path: /-/ready
+            port: 9090
+          initialDelaySeconds: 5
+          periodSeconds: 10
+        livenessProbe:
+          httpGet:
+            path: /-/healthy
+            port: 9090
+          initialDelaySeconds: 30
+          periodSeconds: 15
+        resources:
+          requests:
+            cpu: 100m
+            memory: 256Mi
+          limits:
+            memory: 512Mi
+        volumeMounts:
+        - name: config
+          mountPath: /etc/prometheus
+          readOnly: true
+        - name: storage
+          mountPath: /prometheus
+      volumes:
+      - name: config
+        configMap:
+          name: prometheus
+      - name: storage
+        emptyDir: {}
diff --git a/tests/e2e/dubbod-observability.sh b/tests/e2e/dubbod-observability.sh
new file mode 100755
index 00000000..d3ef0b1d
--- /dev/null
+++ b/tests/e2e/dubbod-observability.sh
@@ -0,0 +1,100 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You 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.
+
+set -euo pipefail
+
+KUBECTL="${KUBECTL:-kubectl}"
+DUBBOD_NAMESPACE="${DUBBOD_NAMESPACE:-dubbo-system}"
+DUBBOD_SERVICE="${DUBBOD_SERVICE:-dubbod}"
+DUBBOD_DEPLOYMENT="${DUBBOD_DEPLOYMENT:-dubbod}"
+ADDONS_DIR="${ADDONS_DIR:-samples/addons}"
+PROMETHEUS_SERVICE="${PROMETHEUS_SERVICE:-prometheus}"
+GRAFANA_SERVICE="${GRAFANA_SERVICE:-grafana}"
+PROMETHEUS_LOCAL_PORT="${PROMETHEUS_LOCAL_PORT:-19090}"
+PROMETHEUS_QUERY="${PROMETHEUS_QUERY:-up{job=\"dubbod\"}}"
+DRY_RUN_ONLY="${DUBBO_OBSERVABILITY_DRY_RUN_ONLY:-false}"
+
+if [[ ! -d "${ADDONS_DIR}" ]]; then
+  echo "addons directory not found: ${ADDONS_DIR}" >&2
+  exit 1
+fi
+
+"${KUBECTL}" apply --dry-run=client -f "${ADDONS_DIR}" >/dev/null
+
+if [[ "${DRY_RUN_ONLY}" == "true" ]]; then
+  exit 0
+fi
+
+"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" rollout status "deploy/${DUBBOD_DEPLOYMENT}" --timeout=180s
+"${KUBECTL}" apply -f "${ADDONS_DIR}"
+"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" rollout status "deploy/${PROMETHEUS_SERVICE}" --timeout=180s
+"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" rollout status "deploy/${GRAFANA_SERVICE}" --timeout=180s
+"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" get "svc/${PROMETHEUS_SERVICE}" "svc/${GRAFANA_SERVICE}" >/dev/null
+
+scrape_annotation="$("${KUBECTL}" -n "${DUBBOD_NAMESPACE}" get "svc/${DUBBOD_SERVICE}" -o jsonpath='{.metadata.annotations.prometheus\.io/scrape}')"
+metrics_port="$("${KUBECTL}" -n "${DUBBOD_NAMESPACE}" get "svc/${DUBBOD_SERVICE}" -o jsonpath='{.metadata.annotations.prometheus\.io/port}')"
+if [[ "${scrape_annotation}" != "true" || "${metrics_port}" != "8080" ]]; then
+  echo "dubbod service prometheus annotations invalid: scrape=${scrape_annotation} port=${metrics_port}" >&2
+  exit 1
+fi
+
+pf_log="$(mktemp -t dubbod-observability-prometheus.XXXXXX.log)"
+"${KUBECTL}" -n "${DUBBOD_NAMESPACE}" port-forward "svc/${PROMETHEUS_SERVICE}" \
+  "${PROMETHEUS_LOCAL_PORT}:9090" >"${pf_log}" 2>&1 &
+pf_pid="$!"
+
+cleanup() {
+  if kill -0 "${pf_pid}" >/dev/null 2>&1; then
+    kill "${pf_pid}" >/dev/null 2>&1 || true
+    wait "${pf_pid}" >/dev/null 2>&1 || true
+  fi
+  rm -f "${pf_log}"
+}
+trap cleanup EXIT
+
+for _ in $(seq 1 100); do
+  if nc -z 127.0.0.1 "${PROMETHEUS_LOCAL_PORT}" >/dev/null 2>&1; then
+    break
+  fi
+  if ! kill -0 "${pf_pid}" >/dev/null 2>&1; then
+    cat "${pf_log}" >&2
+    exit 1
+  fi
+  sleep 0.1
+done
+
+if ! nc -z 127.0.0.1 "${PROMETHEUS_LOCAL_PORT}" >/dev/null 2>&1; then
+  cat "${pf_log}" >&2
+  echo "prometheus port-forward did not become ready" >&2
+  exit 1
+fi
+
+python3 - "${PROMETHEUS_LOCAL_PORT}" "${PROMETHEUS_QUERY}" <<'PY'
+import json
+import sys
+import urllib.parse
+import urllib.request
+
+port, query = sys.argv[1], sys.argv[2]
+url = f"http://127.0.0.1:{port}/api/v1/query?" + urllib.parse.urlencode({"query": query})
+with urllib.request.urlopen(url, timeout=10) as response:
+    payload = json.load(response)
+
+if payload.get("status") != "success":
+    raise SystemExit(f"prometheus query failed: {payload}")
+if not payload.get("data", {}).get("result"):
+    raise SystemExit(f"prometheus query returned no series: {query}")
+PY