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 d47c48d7 Supplement and improve observable construction (#931)
d47c48d7 is described below
commit d47c48d7fa012740d876371ff9f2628c5dc0dda5
Author: mfordjody <[email protected]>
AuthorDate: Tue Jun 30 07:57:00 2026 +0800
Supplement and improve observable construction (#931)
---
cli/cmd/dashboard.go | 22 ++++
cli/cmd/dashboard_test.go | 29 +++++
.../config/kube/gateway/deployment_controller.go | 76 ++++++++++-
.../kube/gateway/deployment_controller_test.go | 108 +++++++++++++++-
manifests/charts/dubbod/files/kube-gateway.yaml | 10 ++
manifests/charts/dubbod/templates/clusterrole.yaml | 1 +
pkg/cni/iptables.go | 6 +
pkg/cni/iptables_test.go | 1 +
pkg/log/log_test.go | 4 +-
pkg/monitoring/monitoring.go | 129 ++++++++++++------
pkg/monitoring/monitoring_test.go | 84 +++++++++++-
samples/addons/grafana.yaml | 144 +++++++++++++++++++++
samples/addons/opentelemetry.yaml | 138 ++++++++++++++++++++
samples/addons/prometheus.yaml | 63 +++++++++
14 files changed, 761 insertions(+), 54 deletions(-)
diff --git a/cli/cmd/dashboard.go b/cli/cmd/dashboard.go
index 2e062f06..c2b0847f 100644
--- a/cli/cmd/dashboard.go
+++ b/cli/cmd/dashboard.go
@@ -102,6 +102,12 @@ func DashboardCmd(ctx cli.Context) *cobra.Command {
}
if dashboardHasTracing(files) {
_, err = fmt.Fprintf(cmd.OutOrStdout(),
"tracing: kubectl -n %s port-forward svc/tracing 16686:16686\n",
dashboardNamespace)
+ if err != nil {
+ return err
+ }
+ }
+ if dashboardHasOpenTelemetry(files) {
+ _, err = fmt.Fprintf(cmd.OutOrStdout(),
"opentelemetry: kubectl -n %s port-forward svc/opentelemetry-collector
4317:4317\n", dashboardNamespace)
}
return err
},
@@ -137,6 +143,10 @@ func dashboardManifestFiles(path string) ([]string, error)
{
if _, err := os.Stat(tracing); err == nil {
files = append(files, tracing)
}
+ opentelemetry := filepath.Join(path, "opentelemetry.yaml")
+ if _, err := os.Stat(opentelemetry); err == nil {
+ files = append(files, opentelemetry)
+ }
return files, nil
}
@@ -149,11 +159,23 @@ func dashboardHasTracing(files []string) bool {
return false
}
+func dashboardHasOpenTelemetry(files []string) bool {
+ for _, file := range files {
+ if filepath.Base(file) == "opentelemetry.yaml" {
+ return true
+ }
+ }
+ return false
+}
+
func dashboardWaitDeploymentNames(files []string) []string {
names := []string{"prometheus", "grafana"}
if dashboardHasTracing(files) {
names = append(names, "tracing")
}
+ if dashboardHasOpenTelemetry(files) {
+ names = append(names, "opentelemetry-collector")
+ }
return names
}
diff --git a/cli/cmd/dashboard_test.go b/cli/cmd/dashboard_test.go
index a087463a..b44a963f 100644
--- a/cli/cmd/dashboard_test.go
+++ b/cli/cmd/dashboard_test.go
@@ -58,6 +58,35 @@ func TestDashboardManifestFilesIncludesTracingWhenPresent(t
*testing.T) {
}
}
+func TestDashboardManifestFilesIncludesOpenTelemetryWhenPresent(t *testing.T) {
+ dir := t.TempDir()
+ for _, name := range []string{"prometheus.yaml", "grafana.yaml",
"tracing.yaml", "opentelemetry.yaml"} {
+ if err := os.WriteFile(filepath.Join(dir, name),
[]byte("apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: "+name+"\n"),
0o600); err != nil {
+ t.Fatal(err)
+ }
+ }
+
+ got, err := dashboardManifestFiles(dir)
+ if err != nil {
+ t.Fatalf("dashboardManifestFiles() returned error: %v", err)
+ }
+ want := []string{
+ filepath.Join(dir, "prometheus.yaml"),
+ filepath.Join(dir, "grafana.yaml"),
+ filepath.Join(dir, "tracing.yaml"),
+ filepath.Join(dir, "opentelemetry.yaml"),
+ }
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("dashboardManifestFiles() = %v, want %v", got, want)
+ }
+ if !dashboardHasOpenTelemetry(got) {
+ t.Fatalf("dashboardHasOpenTelemetry() = false, want true")
+ }
+ if names := dashboardWaitDeploymentNames(got);
!reflect.DeepEqual(names, []string{"prometheus", "grafana", "tracing",
"opentelemetry-collector"}) {
+ t.Fatalf("dashboardWaitDeploymentNames() = %v", names)
+ }
+}
+
func TestReadManifestObjects(t *testing.T) {
file := filepath.Join(t.TempDir(), "manifest.yaml")
manifest := strings.Join([]string{
diff --git a/dubbod/discovery/pkg/config/kube/gateway/deployment_controller.go
b/dubbod/discovery/pkg/config/kube/gateway/deployment_controller.go
index 6b9362ce..85d0cd41 100644
--- a/dubbod/discovery/pkg/config/kube/gateway/deployment_controller.go
+++ b/dubbod/discovery/pkg/config/kube/gateway/deployment_controller.go
@@ -23,6 +23,7 @@ import (
"encoding/hex"
"encoding/json"
"fmt"
+ "math"
"sort"
"strconv"
"strings"
@@ -80,6 +81,10 @@ const (
serviceNodePortAnnotation = "gateway.dubbo.apache.org/node-port"
xdsAddressAnnotation = "gateway.dubbo.apache.org/xds-address"
otelEndpointAnnotation = "gateway.dubbo.apache.org/otel-endpoint"
+ otelSamplingAnnotation =
"gateway.dubbo.apache.org/otel-sampling-percentage"
+ otelServiceNameAnnotation =
"gateway.dubbo.apache.org/otel-service-name"
+ accessLogAnnotation = "gateway.dubbo.apache.org/access-log"
+ accessLogFormatAnnotation =
"gateway.dubbo.apache.org/access-log-format"
)
var builtinClasses = getBuiltinClasses()
@@ -387,6 +392,7 @@ func (d *DeploymentController) configureGateway(log
*dubbolog.Logger, gw gateway
return err
}
+ observability := observabilityConfigForGateway(gw)
input := TemplateInput{
Gateway: &gw,
GatewayClass: string(gw.Spec.GatewayClassName),
@@ -402,7 +408,11 @@ func (d *DeploymentController) configureGateway(log
*dubbolog.Logger, gw gateway
SystemNamespace: d.systemNamespace,
ClusterID: string(d.clusterID),
DomainSuffix: d.domainSuffix(),
- OtelEndpoint:
strings.TrimSpace(gw.Annotations[otelEndpointAnnotation]),
+ OtelEndpoint: observability.OtelEndpoint,
+ OtelServiceName: observability.OtelServiceName,
+ OtelSampling: observability.OtelSampling,
+ AccessLog: observability.AccessLog,
+ AccessLogFormat: observability.AccessLogFormat,
}
log.Infof("desired dxgate deployment=%s/%s gatewayClass=%s
serviceType=%s ports=%s image=%s",
@@ -456,6 +466,65 @@ type TemplateInput struct {
ClusterID string
DomainSuffix string
OtelEndpoint string
+ OtelServiceName string
+ OtelSampling string
+ AccessLog string
+ AccessLogFormat string
+}
+
+type gatewayObservabilityConfig struct {
+ OtelEndpoint string
+ OtelServiceName string
+ OtelSampling string
+ AccessLog string
+ AccessLogFormat string
+}
+
+func observabilityConfigForGateway(gw gateway.Gateway)
gatewayObservabilityConfig {
+ return gatewayObservabilityConfig{
+ OtelEndpoint:
strings.TrimSpace(gw.Annotations[otelEndpointAnnotation]),
+ OtelServiceName: gatewayOtelServiceName(gw),
+ OtelSampling: gatewayOtelSampling(gw),
+ AccessLog: gatewayAccessLog(gw),
+ AccessLogFormat: gatewayAccessLogFormat(gw),
+ }
+}
+
+func gatewayOtelServiceName(gw gateway.Gateway) string {
+ if value :=
strings.TrimSpace(gw.Annotations[otelServiceNameAnnotation]); value != "" {
+ return value
+ }
+ return fmt.Sprintf("dxgate.%s.%s", gw.Namespace, gw.Name)
+}
+
+func gatewayOtelSampling(gw gateway.Gateway) string {
+ value := strings.TrimSpace(gw.Annotations[otelSamplingAnnotation])
+ if value == "" {
+ return "100"
+ }
+ parsed, err := strconv.ParseFloat(value, 64)
+ if err != nil || math.IsNaN(parsed) || math.IsInf(parsed, 0) || parsed
< 0 || parsed > 100 {
+ return "100"
+ }
+ return strconv.FormatFloat(parsed, 'f', -1, 64)
+}
+
+func gatewayAccessLog(gw gateway.Gateway) string {
+ switch
strings.ToLower(strings.TrimSpace(gw.Annotations[accessLogAnnotation])) {
+ case "false", "0", "no", "off":
+ return "false"
+ default:
+ return "true"
+ }
+}
+
+func gatewayAccessLogFormat(gw gateway.Gateway) string {
+ switch
strings.ToLower(strings.TrimSpace(gw.Annotations[accessLogFormatAnnotation])) {
+ case "json":
+ return "json"
+ default:
+ return "text"
+ }
}
type dxgateBootstrapConfig struct {
@@ -1366,10 +1435,7 @@ func gatewayServiceTargetPort(gw gateway.Gateway)
intstr.IntOrString {
if port, ok := positiveIntAnnotation(gw, serviceTargetPortAnnotation);
ok {
return intstr.FromInt(port)
}
- if gw.Annotations[eastWestGatewayAnnotation] == "true" {
- return intstr.FromInt(inject.ProxylessGRPCInboundPort)
- }
- return intstr.FromString("http")
+ return intstr.FromInt(inject.ProxylessGRPCInboundPort)
}
func gatewayServiceNodePort(gw gateway.Gateway) int32 {
diff --git
a/dubbod/discovery/pkg/config/kube/gateway/deployment_controller_test.go
b/dubbod/discovery/pkg/config/kube/gateway/deployment_controller_test.go
index da3cef09..6d273c81 100644
--- a/dubbod/discovery/pkg/config/kube/gateway/deployment_controller_test.go
+++ b/dubbod/discovery/pkg/config/kube/gateway/deployment_controller_test.go
@@ -440,7 +440,7 @@ func
TestDeploymentControllerBuildDxgateBootstrapConfigUsesXDSAddressAnnotation(
}
}
-func TestExtractServicePortsTargetsDxgateContainerPorts(t *testing.T) {
+func TestExtractServicePortsTargetsGRPCInbound(t *testing.T) {
gw := gatewayv1.Gateway{
Spec: gatewayv1.GatewaySpec{
Listeners: []gatewayv1.Listener{
@@ -454,7 +454,7 @@ func TestExtractServicePortsTargetsDxgateContainerPorts(t
*testing.T) {
if len(ports) != 1 {
t.Fatalf("expected only the http listener port, got %#v", ports)
}
- if ports[0].Name != "http" || ports[0].Port != 8080 ||
ports[0].TargetPort.String() != "http" {
+ if ports[0].Name != "http" || ports[0].Port != 8080 ||
ports[0].TargetPort.IntValue() != 15080 {
t.Fatalf("unexpected http service port: %#v", ports[0])
}
}
@@ -498,6 +498,85 @@ func TestServiceTypeForGatewayUsesAnnotation(t *testing.T)
{
}
}
+func TestObservabilityConfigForGatewayDefaults(t *testing.T) {
+ cfg := observabilityConfigForGateway(gatewayv1.Gateway{
+ ObjectMeta: metav1.ObjectMeta{Name: "public", Namespace: "app"},
+ })
+
+ if cfg.OtelEndpoint != "" {
+ t.Fatalf("otel endpoint = %q, want empty", cfg.OtelEndpoint)
+ }
+ if cfg.OtelServiceName != "dxgate.app.public" {
+ t.Fatalf("otel service name = %q", cfg.OtelServiceName)
+ }
+ if cfg.OtelSampling != "100" {
+ t.Fatalf("otel sampling = %q", cfg.OtelSampling)
+ }
+ if cfg.AccessLog != "true" {
+ t.Fatalf("access log = %q", cfg.AccessLog)
+ }
+ if cfg.AccessLogFormat != "text" {
+ t.Fatalf("access log format = %q", cfg.AccessLogFormat)
+ }
+}
+
+func TestObservabilityConfigForGatewayAnnotations(t *testing.T) {
+ cfg := observabilityConfigForGateway(gatewayv1.Gateway{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "public",
+ Namespace: "app",
+ Annotations: map[string]string{
+ otelEndpointAnnotation: "
http://otel-collector.dubbo-system.svc:4317 ",
+ otelServiceNameAnnotation: "edge-gateway",
+ otelSamplingAnnotation: "12.5",
+ accessLogAnnotation: "false",
+ accessLogFormatAnnotation: "json",
+ },
+ },
+ })
+
+ if cfg.OtelEndpoint != "http://otel-collector.dubbo-system.svc:4317" {
+ t.Fatalf("otel endpoint = %q", cfg.OtelEndpoint)
+ }
+ if cfg.OtelServiceName != "edge-gateway" {
+ t.Fatalf("otel service name = %q", cfg.OtelServiceName)
+ }
+ if cfg.OtelSampling != "12.5" {
+ t.Fatalf("otel sampling = %q", cfg.OtelSampling)
+ }
+ if cfg.AccessLog != "false" {
+ t.Fatalf("access log = %q", cfg.AccessLog)
+ }
+ if cfg.AccessLogFormat != "json" {
+ t.Fatalf("access log format = %q", cfg.AccessLogFormat)
+ }
+}
+
+func TestObservabilityConfigForGatewayInvalidSamplingFallsBack(t *testing.T) {
+ for _, value := range []string{"abc", "-1", "101", "NaN", "+Inf"} {
+ cfg := observabilityConfigForGateway(gatewayv1.Gateway{
+ ObjectMeta: metav1.ObjectMeta{
+ Name: "public",
+ Namespace: "app",
+ Annotations: map[string]string{
+ otelSamplingAnnotation: value,
+ accessLogAnnotation: "maybe",
+ accessLogFormatAnnotation: "yaml",
+ },
+ },
+ })
+ if cfg.OtelSampling != "100" {
+ t.Fatalf("otel sampling for %q = %q, want 100", value,
cfg.OtelSampling)
+ }
+ if cfg.AccessLog != "true" {
+ t.Fatalf("access log for %q = %q, want true", value,
cfg.AccessLog)
+ }
+ if cfg.AccessLogFormat != "text" {
+ t.Fatalf("access log format for %q = %q, want text",
value, cfg.AccessLogFormat)
+ }
+ }
+}
+
func TestGetDefaultNameUsesFixedDxgateGatewayName(t *testing.T) {
spec := &gatewayv1.GatewaySpec{GatewayClassName: "dubbo"}
if got := getDefaultName("httpbin-gateway", spec, false); got !=
"dxgate-gateway" {
@@ -537,7 +616,7 @@ func TestKubeGatewayTemplateRendersDxgateResources(t
*testing.T) {
},
DeploymentName: "public-dubbo",
ServiceAccount: "public-dubbo",
- Ports: []corev1.ServicePort{{Name: "http", Port:
80, TargetPort: intstr.FromString("http")}},
+ Ports: []corev1.ServicePort{{Name: "http", Port:
80, TargetPort: intstr.FromInt(15080)}},
ServiceType: corev1.ServiceTypeLoadBalancer,
Revision: "default",
BootstrapConfig: "{\n \"xds_address\":
\"http://dubbod.dubbo-system.svc:26010\",\n \"listener_names\": [\n
\"public-dubbo.app.svc.cluster.local:80\"\n ],\n \"cluster_id\":
\"Kubernetes\",\n \"dns_domain\": \"cluster.local\"\n}\n",
@@ -546,6 +625,10 @@ func TestKubeGatewayTemplateRendersDxgateResources(t
*testing.T) {
SystemNamespace: "dubbo-system",
ClusterID: "Kubernetes",
DomainSuffix: "cluster.local",
+ OtelServiceName: "dxgate.app.public",
+ OtelSampling: "100",
+ AccessLog: "true",
+ AccessLogFormat: "text",
}
rendered, err := controller.render("gateway", input)
if err != nil {
@@ -586,11 +669,26 @@ func TestKubeGatewayTemplateRendersDxgateResources(t
*testing.T) {
if strings.Contains(rendered[2], "DXGATE_OTEL_ENDPOINT") {
t.Fatalf("deployment rendered OTEL endpoint without
annotation:\n%s", rendered[2])
}
+ for _, want := range []string{
+ "DXGATE_GATEWAY_NAME",
+ `value: "public"`,
+ "DXGATE_OTEL_SERVICE_NAME",
+ `value: "dxgate.app.public"`,
+ "DXGATE_OTEL_SAMPLING_PERCENTAGE",
+ `value: "100"`,
+ "DXGATE_ACCESS_LOG",
+ "DXGATE_ACCESS_LOG_FORMAT",
+ `value: "text"`,
+ } {
+ if !strings.Contains(rendered[2], want) {
+ t.Fatalf("deployment did not render observability env
%q:\n%s", want, rendered[2])
+ }
+ }
if !strings.Contains(rendered[2], "app.kubernetes.io/instance:
public-dubbo") {
t.Fatalf("deployment did not render stable dxgate instance
label:\n%s", rendered[2])
}
- if !strings.Contains(rendered[3], "targetPort: http") {
- t.Fatalf("service did not target dxgate http port:\n%s",
rendered[3])
+ if !strings.Contains(rendered[3], "targetPort: 15080") {
+ t.Fatalf("service did not target grpc-inbound port:\n%s",
rendered[3])
}
if !strings.Contains(rendered[3], `proxyless.dubbo.apache.org/inject:
"false"`) {
t.Fatalf("service did not opt out of proxyless targetPort
rewriting:\n%s", rendered[3])
diff --git a/manifests/charts/dubbod/files/kube-gateway.yaml
b/manifests/charts/dubbod/files/kube-gateway.yaml
index d0962904..6c10353a 100644
--- a/manifests/charts/dubbod/files/kube-gateway.yaml
+++ b/manifests/charts/dubbod/files/kube-gateway.yaml
@@ -84,10 +84,20 @@ spec:
value: 0.0.0.0:8080
- name: DXGATE_ADMIN_ADDR
value: 0.0.0.0:26021
+ - name: DXGATE_GATEWAY_NAME
+ value: {{ .Gateway.Name | quote }}
+ - name: DXGATE_OTEL_SERVICE_NAME
+ value: {{ .OtelServiceName | quote }}
+ - name: DXGATE_OTEL_SAMPLING_PERCENTAGE
+ value: {{ .OtelSampling | quote }}
{{- if .OtelEndpoint }}
- name: DXGATE_OTEL_ENDPOINT
value: {{ .OtelEndpoint | quote }}
{{- end }}
+ - name: DXGATE_ACCESS_LOG
+ value: {{ .AccessLog | quote }}
+ - name: DXGATE_ACCESS_LOG_FORMAT
+ value: {{ .AccessLogFormat | quote }}
- name: POD_NAME
valueFrom:
fieldRef:
diff --git a/manifests/charts/dubbod/templates/clusterrole.yaml
b/manifests/charts/dubbod/templates/clusterrole.yaml
index 5e525070..4f0afd13 100644
--- a/manifests/charts/dubbod/templates/clusterrole.yaml
+++ b/manifests/charts/dubbod/templates/clusterrole.yaml
@@ -62,6 +62,7 @@ rules:
- gateways
- gatewayclasses
- httproutes
+ - backendtlspolicies
verbs:
- get
- list
diff --git a/pkg/cni/iptables.go b/pkg/cni/iptables.go
index 9829b8b5..bad4eec5 100644
--- a/pkg/cni/iptables.go
+++ b/pkg/cni/iptables.go
@@ -26,6 +26,7 @@ import (
const (
meshInboundChain = "DUBBO-GRPC-INBOUND"
meshPodIPSet = "DUBBO-GRPC-INBOUND-PODS"
+ dxgateAdminPort = 26021
)
type CommandRunner interface {
@@ -98,12 +99,17 @@ func (m *IPTablesRuleManager) ensureBase(ctx
context.Context) error {
}
}
allowGRPCInbound := []string{"-m", "set", "--match-set", meshPodIPSet,
"dst", "-p", "tcp", "--dport", fmt.Sprint(m.grpcInboundPort), "-j", "RETURN"}
+ allowDxgateAdmin := []string{"-m", "set", "--match-set", meshPodIPSet,
"dst", "-p", "tcp", "--dport", fmt.Sprint(dxgateAdminPort), "-j", "RETURN"}
rejectOtherTCP := []string{"-m", "set", "--match-set", meshPodIPSet,
"dst", "-p", "tcp", "-j", "REJECT"}
m.deleteRepeated(ctx, allowGRPCInbound...)
+ m.deleteRepeated(ctx, allowDxgateAdmin...)
m.deleteRepeated(ctx, rejectOtherTCP...)
if err := m.appendRule(ctx, allowGRPCInbound...); err != nil {
return err
}
+ if err := m.appendRule(ctx, allowDxgateAdmin...); err != nil {
+ return err
+ }
return m.appendRule(ctx, rejectOtherTCP...)
}
diff --git a/pkg/cni/iptables_test.go b/pkg/cni/iptables_test.go
index 56cc67fc..ea8af9f6 100644
--- a/pkg/cni/iptables_test.go
+++ b/pkg/cni/iptables_test.go
@@ -40,6 +40,7 @@ func TestIPTablesRuleManagerAddsGRPCInboundBoundaryRules(t
*testing.T) {
"-I FORWARD 1 -j DUBBO-GRPC-INBOUND",
"-I OUTPUT 1 -j DUBBO-GRPC-INBOUND",
"-A DUBBO-GRPC-INBOUND -m set --match-set
DUBBO-GRPC-INBOUND-PODS dst -p tcp --dport 15080 -j RETURN",
+ "-A DUBBO-GRPC-INBOUND -m set --match-set
DUBBO-GRPC-INBOUND-PODS dst -p tcp --dport 26021 -j RETURN",
"-A DUBBO-GRPC-INBOUND -m set --match-set
DUBBO-GRPC-INBOUND-PODS dst -p tcp -j REJECT",
"ipset add DUBBO-GRPC-INBOUND-PODS 10.244.0.12 -exist",
} {
diff --git a/pkg/log/log_test.go b/pkg/log/log_test.go
index 5114d30c..97f474ef 100644
--- a/pkg/log/log_test.go
+++ b/pkg/log/log_test.go
@@ -123,8 +123,8 @@ func TestLogMessagesMetricRecordsLevelAndScope(t
*testing.T) {
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())
+ if metric.GetCounter().GetValue() < 1 {
+ t.Fatalf("dubbod_log_messages_total =
%v, want >= 1", metric.GetCounter().GetValue())
}
return
}
diff --git a/pkg/monitoring/monitoring.go b/pkg/monitoring/monitoring.go
index 35603243..6f6bf3be 100644
--- a/pkg/monitoring/monitoring.go
+++ b/pkg/monitoring/monitoring.go
@@ -138,7 +138,8 @@ type metric struct {
description string
valueType prometheus.ValueType
labelNames []string
- vec *prometheus.GaugeVec
+ counterVec *prometheus.CounterVec
+ gaugeVec *prometheus.GaugeVec
vecOnce sync.Once
counter prometheus.Counter
gauge prometheus.Gauge
@@ -160,36 +161,20 @@ func newMetric(name, description string, valueType
prometheus.ValueType, opts ..
// If labels are predefined, create the vector immediately
if len(o.labels) > 0 {
if valueType == prometheus.CounterValue {
- m.vec = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ m.counterVec =
prometheus.NewCounterVec(prometheus.CounterOpts{
Name: name,
Help: description,
}, o.labels)
- } else {
- m.vec = prometheus.NewGaugeVec(prometheus.GaugeOpts{
- Name: name,
- Help: description,
- }, o.labels)
- }
- registryLock.Lock()
- registry.MustRegister(m.vec)
- registryLock.Unlock()
- } else {
- // No predefined labels, create scalar metric
- if valueType == prometheus.CounterValue {
- m.counter =
prometheus.NewCounter(prometheus.CounterOpts{
- Name: name,
- Help: description,
- })
registryLock.Lock()
- registry.MustRegister(m.counter)
+ registry.MustRegister(m.counterVec)
registryLock.Unlock()
} else {
- m.gauge = prometheus.NewGauge(prometheus.GaugeOpts{
+ m.gaugeVec =
prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: name,
Help: description,
- })
+ }, o.labels)
registryLock.Lock()
- registry.MustRegister(m.gauge)
+ registry.MustRegister(m.gaugeVec)
registryLock.Unlock()
}
}
@@ -210,7 +195,32 @@ func (m *metric) Name() string {
}
func (m *metric) Record(value float64) {
+ if m.counterVec != nil || m.gaugeVec != nil {
+ return
+ }
+ m.vecOnce.Do(func() {
+ if m.valueType == prometheus.CounterValue {
+ m.counter =
prometheus.NewCounter(prometheus.CounterOpts{
+ Name: m.name,
+ Help: m.description,
+ })
+ registryLock.Lock()
+ registry.MustRegister(m.counter)
+ registryLock.Unlock()
+ } else {
+ m.gauge = prometheus.NewGauge(prometheus.GaugeOpts{
+ Name: m.name,
+ Help: m.description,
+ })
+ registryLock.Lock()
+ registry.MustRegister(m.gauge)
+ registryLock.Unlock()
+ }
+ })
if m.counter != nil {
+ if value < 0 {
+ return
+ }
m.counter.Add(value)
} else if m.gauge != nil {
if value >= 0 {
@@ -227,14 +237,25 @@ func (m *metric) RecordInt(value int64) {
func (m *metric) With(labelValues ...LabelValue) Metric {
// If vector already exists (predefined labels), use it directly
- if m.vec != nil {
+ if m.counterVec != nil {
+ labels := make(prometheus.Labels)
+ for _, lv := range labelValues {
+ labels[lv.Name] = lv.Value
+ }
+ return &labeledMetric{
+ parent: m,
+ counter: m.counterVec.With(labels),
+ labels: labels,
+ }
+ }
+ if m.gaugeVec != nil {
labels := make(prometheus.Labels)
for _, lv := range labelValues {
labels[lv.Name] = lv.Value
}
return &labeledMetric{
parent: m,
- gauge: m.vec.With(labels),
+ gauge: m.gaugeVec.With(labels),
labels: labels,
}
}
@@ -247,20 +268,22 @@ func (m *metric) With(labelValues ...LabelValue) Metric {
}
if m.valueType == prometheus.CounterValue {
- m.vec = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ m.counterVec =
prometheus.NewCounterVec(prometheus.CounterOpts{
Name: m.name,
Help: m.description,
}, labelNames)
+ registryLock.Lock()
+ registry.MustRegister(m.counterVec)
+ registryLock.Unlock()
} else {
- m.vec = prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ m.gaugeVec =
prometheus.NewGaugeVec(prometheus.GaugeOpts{
Name: m.name,
Help: m.description,
}, labelNames)
+ registryLock.Lock()
+ registry.MustRegister(m.gaugeVec)
+ registryLock.Unlock()
}
-
- registryLock.Lock()
- registry.MustRegister(m.vec)
- registryLock.Unlock()
})
labels := make(prometheus.Labels)
@@ -269,16 +292,32 @@ func (m *metric) With(labelValues ...LabelValue) Metric {
}
return &labeledMetric{
- parent: m,
- gauge: m.vec.With(labels),
- labels: labels,
+ parent: m,
+ counter: m.counterWith(labels),
+ gauge: m.gaugeWith(labels),
+ labels: labels,
}
}
type labeledMetric struct {
- parent *metric
- gauge prometheus.Gauge
- labels prometheus.Labels
+ parent *metric
+ counter prometheus.Counter
+ gauge prometheus.Gauge
+ labels prometheus.Labels
+}
+
+func (m *metric) counterWith(labels prometheus.Labels) prometheus.Counter {
+ if m.counterVec == nil {
+ return nil
+ }
+ return m.counterVec.With(labels)
+}
+
+func (m *metric) gaugeWith(labels prometheus.Labels) prometheus.Gauge {
+ if m.gaugeVec == nil {
+ return nil
+ }
+ return m.gaugeVec.With(labels)
}
func (lm *labeledMetric) Increment() {
@@ -295,7 +334,10 @@ func (lm *labeledMetric) Name() string {
func (lm *labeledMetric) Record(value float64) {
if lm.parent.valueType == prometheus.CounterValue {
- lm.gauge.Add(value)
+ if lm.counter == nil || value < 0 {
+ return
+ }
+ lm.counter.Add(value)
} else {
if value >= 0 {
lm.gauge.Set(value)
@@ -320,7 +362,18 @@ func (lm *labeledMetric) With(labelValues ...LabelValue)
Metric {
return &labeledMetric{
parent: lm.parent,
- gauge: lm.parent.vec.With(newLabels),
+ counter: func() prometheus.Counter {
+ if lm.parent.counterVec == nil {
+ return nil
+ }
+ return lm.parent.counterVec.With(newLabels)
+ }(),
+ gauge: func() prometheus.Gauge {
+ if lm.parent.gaugeVec == nil {
+ return nil
+ }
+ return lm.parent.gaugeVec.With(newLabels)
+ }(),
labels: newLabels,
}
}
diff --git a/pkg/monitoring/monitoring_test.go
b/pkg/monitoring/monitoring_test.go
index 91bf05d2..c3554114 100644
--- a/pkg/monitoring/monitoring_test.go
+++ b/pkg/monitoring/monitoring_test.go
@@ -20,8 +20,54 @@ import (
"fmt"
"testing"
"time"
+
+ dto "github.com/prometheus/client_model/go"
)
+func TestNewSumWithoutLabelsExposesCounter(t *testing.T) {
+ name := fmt.Sprintf("test_sum_without_labels_total_%d",
time.Now().UnixNano())
+ metric := NewSum(name, "Test scalar counter.")
+
+ metric.Increment()
+ metric.RecordInt(2)
+
+ family := findMetricFamily(t, name)
+ if family.GetType() != dto.MetricType_COUNTER {
+ t.Fatalf("metric type = %s, want COUNTER", family.GetType())
+ }
+ if got := family.GetMetric()[0].GetCounter().GetValue(); got != 3 {
+ t.Fatalf("counter value = %v, want 3", got)
+ }
+}
+
+func TestNewSumWithPredeclaredLabelsExposesCounterVec(t *testing.T) {
+ phaseTag := CreateLabel("phase")
+ name := fmt.Sprintf("test_sum_with_labels_total_%d",
time.Now().UnixNano())
+ metric := NewSum(name, "Test labeled counter.", WithLabels("phase"))
+
+ metric.With(phaseTag.Value("push")).Increment()
+ metric.With(phaseTag.Value("send")).RecordInt(2)
+
+ assertCounterSeries(t, name, map[string]float64{
+ "push": 1,
+ "send": 2,
+ })
+}
+
+func TestNewSumWithDynamicLabelsExposesCounterVec(t *testing.T) {
+ typeTag := CreateLabel("type")
+ name := fmt.Sprintf("test_sum_with_dynamic_labels_total_%d",
time.Now().UnixNano())
+ metric := NewSum(name, "Test dynamically labeled counter.")
+
+ metric.With(typeTag.Value("cds")).Increment()
+ metric.With(typeTag.Value("eds")).RecordInt(4)
+
+ assertCounterSeries(t, name, map[string]float64{
+ "cds": 1,
+ "eds": 4,
+ })
+}
+
func TestDistributionWithLabelsRegistersOnce(t *testing.T) {
phaseTag := CreateLabel("phase")
name := fmt.Sprintf("test_distribution_with_labels_seconds_%d",
time.Now().UnixNano())
@@ -35,17 +81,47 @@ func TestDistributionWithLabelsRegistersOnce(t *testing.T) {
metric.With(phaseTag.Value("push")).Record(0.2)
metric.With(phaseTag.Value("send")).Record(0.4)
+ family := findMetricFamily(t, name)
+ if got := len(family.GetMetric()); got != 2 {
+ t.Fatalf("metric series = %d, want 2", got)
+ }
+}
+
+func findMetricFamily(t *testing.T, name string) *dto.MetricFamily {
+ t.Helper()
families, err := GetRegistry().Gather()
if err != nil {
t.Fatalf("gather metrics: %v", err)
}
for _, family := range families {
if family.GetName() == name {
- if got := len(family.GetMetric()); got != 2 {
- t.Fatalf("metric series = %d, want 2", got)
- }
- return
+ return family
}
}
t.Fatalf("%s not gathered", name)
+ return nil
+}
+
+func assertCounterSeries(t *testing.T, name string, want map[string]float64) {
+ t.Helper()
+ family := findMetricFamily(t, name)
+ if family.GetType() != dto.MetricType_COUNTER {
+ t.Fatalf("metric type = %s, want COUNTER", family.GetType())
+ }
+ got := map[string]float64{}
+ for _, metric := range family.GetMetric() {
+ for _, label := range metric.GetLabel() {
+ if label.GetName() == "phase" || label.GetName() ==
"type" {
+ got[label.GetValue()] =
metric.GetCounter().GetValue()
+ }
+ }
+ }
+ if len(got) != len(want) {
+ t.Fatalf("counter series = %#v, want %#v", got, want)
+ }
+ for label, value := range want {
+ if got[label] != value {
+ t.Fatalf("counter series %s = %v, want %v", label,
got[label], value)
+ }
+ }
}
diff --git a/samples/addons/grafana.yaml b/samples/addons/grafana.yaml
index 406eebd8..b9a56ef6 100644
--- a/samples/addons/grafana.yaml
+++ b/samples/addons/grafana.yaml
@@ -961,6 +961,150 @@ data:
],
"title": "HTTP Upstream P95",
"type": "timeseries"
+ },
+ {
+ "datasource": {
+ "type": "prometheus",
+ "uid": "prometheus"
+ },
+ "fieldConfig": {
+ "defaults": {
+ "custom": {
+ "drawStyle": "line",
+ "fillOpacity": 10,
+ "lineWidth": 1,
+ "showPoints": "never"
+ },
+ "unit": "ms"
+ },
+ "overrides": []
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 8,
+ "x": 0,
+ "y": 49
+ },
+ "id": 23,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "expr": "histogram_quantile(0.99, sum by (le, route, cluster)
(rate(dxgate_http_route_latency_ms_bucket[5m])))",
+ "legendFormat": "{{route}} / {{cluster}}",
+ "refId": "A"
+ }
+ ],
+ "title": "HTTP Upstream P99",
+ "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": 49
+ },
+ "id": 24,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "expr": "sum by (route, cluster, status_code)
(rate(dxgate_http_route_requests_total{status_code=~\"5..\"}[5m]))",
+ "legendFormat": "{{route}} / {{cluster}} / {{status_code}}",
+ "refId": "A"
+ }
+ ],
+ "title": "HTTP 5xx 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": 49
+ },
+ "id": 25,
+ "options": {
+ "legend": {
+ "calcs": [
+ "lastNotNull"
+ ],
+ "displayMode": "table",
+ "placement": "bottom",
+ "showLegend": true
+ },
+ "tooltip": {
+ "mode": "multi",
+ "sort": "none"
+ }
+ },
+ "targets": [
+ {
+ "expr": "sum by (route, cluster, status_code)
(rate(dxgate_http_route_requests_total{status_code=~\"401|403|429\"}[5m]))",
+ "legendFormat": "{{route}} / {{cluster}} / {{status_code}}",
+ "refId": "A"
+ }
+ ],
+ "title": "Policy Rejection Rate",
+ "type": "timeseries"
}
],
"refresh": "15s",
diff --git a/samples/addons/opentelemetry.yaml
b/samples/addons/opentelemetry.yaml
new file mode 100644
index 00000000..cb22d666
--- /dev/null
+++ b/samples/addons/opentelemetry.yaml
@@ -0,0 +1,138 @@
+# 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: opentelemetry-collector
+ namespace: dubbo-system
+ labels:
+ app.kubernetes.io/name: opentelemetry-collector
+ app.kubernetes.io/part-of: dubbo-observability
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: opentelemetry-collector
+ namespace: dubbo-system
+ labels:
+ app.kubernetes.io/name: opentelemetry-collector
+ app.kubernetes.io/part-of: dubbo-observability
+data:
+ collector.yaml: |
+ receivers:
+ otlp:
+ protocols:
+ grpc:
+ endpoint: 0.0.0.0:4317
+ http:
+ endpoint: 0.0.0.0:4318
+
+ processors:
+ batch: {}
+
+ exporters:
+ debug:
+ verbosity: basic
+
+ service:
+ pipelines:
+ traces:
+ receivers: [otlp]
+ processors: [batch]
+ exporters: [debug]
+ logs:
+ receivers: [otlp]
+ processors: [batch]
+ exporters: [debug]
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: opentelemetry-collector
+ namespace: dubbo-system
+ labels:
+ app.kubernetes.io/name: opentelemetry-collector
+ app.kubernetes.io/part-of: dubbo-observability
+spec:
+ selector:
+ app.kubernetes.io/name: opentelemetry-collector
+ ports:
+ - name: grpc-otlp
+ port: 4317
+ targetPort: grpc-otlp
+ protocol: TCP
+ - name: http-otlp
+ port: 4318
+ targetPort: http-otlp
+ protocol: TCP
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: opentelemetry-collector
+ namespace: dubbo-system
+ labels:
+ app.kubernetes.io/name: opentelemetry-collector
+ app.kubernetes.io/part-of: dubbo-observability
+spec:
+ replicas: 1
+ selector:
+ matchLabels:
+ app.kubernetes.io/name: opentelemetry-collector
+ template:
+ metadata:
+ labels:
+ app.kubernetes.io/name: opentelemetry-collector
+ app.kubernetes.io/part-of: dubbo-observability
+ spec:
+ serviceAccountName: opentelemetry-collector
+ containers:
+ - name: collector
+ image: otel/opentelemetry-collector-contrib:0.128.0
+ imagePullPolicy: IfNotPresent
+ args:
+ - --config=/etc/otelcol/collector.yaml
+ ports:
+ - name: grpc-otlp
+ containerPort: 4317
+ protocol: TCP
+ - name: http-otlp
+ containerPort: 4318
+ protocol: TCP
+ readinessProbe:
+ tcpSocket:
+ port: grpc-otlp
+ initialDelaySeconds: 5
+ periodSeconds: 10
+ livenessProbe:
+ tcpSocket:
+ port: grpc-otlp
+ initialDelaySeconds: 20
+ periodSeconds: 15
+ resources:
+ requests:
+ cpu: 100m
+ memory: 128Mi
+ limits:
+ memory: 512Mi
+ volumeMounts:
+ - name: config
+ mountPath: /etc/otelcol
+ readOnly: true
+ volumes:
+ - name: config
+ configMap:
+ name: opentelemetry-collector
diff --git a/samples/addons/prometheus.yaml b/samples/addons/prometheus.yaml
index 17f1d459..4301ab58 100644
--- a/samples/addons/prometheus.yaml
+++ b/samples/addons/prometheus.yaml
@@ -64,6 +64,9 @@ data:
scrape_interval: 15s
evaluation_interval: 15s
+ rule_files:
+ - /etc/prometheus/rules/*.yaml
+
scrape_configs:
- job_name: prometheus
static_configs:
@@ -103,6 +106,60 @@ data:
target_label: app
---
apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: prometheus-rules
+ namespace: dubbo-system
+ labels:
+ app.kubernetes.io/name: prometheus
+ app.kubernetes.io/part-of: dubbo-observability
+data:
+ dubbo-alerts.yaml: |
+ groups:
+ - name: dubbo-observability
+ rules:
+ - alert: Dxgate5xxRateHigh
+ expr: sum by (namespace, gateway, route, cluster)
(rate(dxgate_http_route_requests_total{status_code=~"5.."}[5m])) > 0
+ for: 5m
+ labels:
+ severity: warning
+ annotations:
+ summary: dxgate 5xx responses are increasing
+ description: dxgate is returning 5xx responses for one or more
Gateway routes.
+ - alert: DxgateP95LatencyHigh
+ expr: histogram_quantile(0.95, sum by (le, namespace, gateway, route,
cluster) (rate(dxgate_http_route_latency_ms_bucket[5m]))) > 1000
+ for: 10m
+ labels:
+ severity: warning
+ annotations:
+ summary: dxgate P95 latency is above 1s
+ description: Gateway upstream latency is above the sample threshold.
+ - alert: DxgateNotReady
+ expr: up{app="dxgate"} == 0
+ for: 2m
+ labels:
+ severity: critical
+ annotations:
+ summary: dxgate metrics target is down
+ description: Prometheus cannot scrape a dxgate pod or the pod is not
ready.
+ - alert: DubbodXDSRejects
+ expr: sum(rate(dubbod_total_xds_rejects[5m])) > 0
+ for: 5m
+ labels:
+ severity: warning
+ annotations:
+ summary: xDS rejects are increasing
+ description: One or more data-plane clients rejected xDS resources
from dubbod.
+ - alert: DubbodSDSError
+ expr: sum(rate(dubbod_sds_certificate_errors_total[5m])) > 0
+ for: 5m
+ labels:
+ severity: warning
+ annotations:
+ summary: SDS certificate errors are increasing
+ description: Workload certificate fetch or delivery is failing.
+---
+apiVersion: v1
kind: Service
metadata:
name: prometheus
@@ -173,11 +230,17 @@ spec:
- name: config
mountPath: /etc/prometheus
readOnly: true
+ - name: rules
+ mountPath: /etc/prometheus/rules
+ readOnly: true
- name: storage
mountPath: /prometheus
volumes:
- name: config
configMap:
name: prometheus
+ - name: rules
+ configMap:
+ name: prometheus-rules
- name: storage
emptyDir: {}