From a2551ad0460fc171d3513f97fc666847591080e1 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Wed, 30 Sep 2026 17:05:34 -0500 Subject: [PATCH 1/4] feat(serviceroute): add controller and dataplane observability --- cmd/galactic-router/root.go | 12 ++- .../controller/serviceroute_controller.go | 68 +++++++++++++-- internal/serviceroute/metrics.go | 82 +++++++++++++++++++ 3 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 internal/serviceroute/metrics.go diff --git a/cmd/galactic-router/root.go b/cmd/galactic-router/root.go index 9461838f..5d2876b0 100644 --- a/cmd/galactic-router/root.go +++ b/cmd/galactic-router/root.go @@ -23,6 +23,7 @@ import ( clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/log/zap" + ctrlmetrics "sigs.k8s.io/controller-runtime/pkg/metrics" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" "sigs.k8s.io/controller-runtime/pkg/webhook" @@ -106,6 +107,8 @@ func runCmd(cfg *config.RouterConfig) error { if err != nil { return fmt.Errorf("create manager: %w", err) } + serviceRouteMetrics := serviceroute.NewMetrics() + serviceRouteMetrics.MustRegister(ctrlmetrics.Registry) // ctx carries a cause so an ordinary signal-triggered shutdown can be told // apart from the health server's own Serve failure below. See the cause @@ -278,7 +281,7 @@ func runCmd(cfg *config.RouterConfig) error { // controller process before informer reconciliation starts. A missing map is // tolerated because the datapath loader may start after galactic-router; // the first Apply retries initialization. - if err := setupServiceRouteController(mgr, nodeName, cfg.GCNamespace); err != nil { + if err := setupServiceRouteController(mgr, nodeName, cfg.GCNamespace, serviceRouteMetrics); err != nil { return fmt.Errorf("setup ServiceRoutePolicy controller: %w", err) } @@ -346,7 +349,11 @@ func runCmd(cfg *config.RouterConfig) error { return nil } -func setupServiceRouteController(mgr ctrl.Manager, nodeName, bgpNamespace string) error { +func setupServiceRouteController( + mgr ctrl.Manager, + nodeName, bgpNamespace string, + serviceRouteMetrics *serviceroute.Metrics, +) error { programmer := &serviceroute.EBPFRouteProgrammer{} if err := programmer.Initialize(); err != nil { ctrl.Log.Error(err, "service route eBPF maps are not available yet; initialization will retry on reconcile") @@ -358,6 +365,7 @@ func setupServiceRouteController(mgr ctrl.Manager, nodeName, bgpNamespace string NodeName: nodeName, BGPNamespace: bgpNamespace, Programmer: programmer, + Metrics: serviceRouteMetrics, Applied: make(map[types.NamespacedName]map[types.NamespacedName]serviceroute.RouteIntent), }).SetupWithManager(mgr) } diff --git a/internal/controller/serviceroute_controller.go b/internal/controller/serviceroute_controller.go index 8015338e..3faa82e4 100644 --- a/internal/controller/serviceroute_controller.go +++ b/internal/controller/serviceroute_controller.go @@ -43,6 +43,7 @@ type ServiceRoutePolicyReconciler struct { NodeName string BGPNamespace string Programmer serviceroute.RouteProgrammer + Metrics *serviceroute.Metrics mu sync.Mutex Applied map[types.NamespacedName]map[types.NamespacedName]serviceroute.RouteIntent @@ -83,29 +84,50 @@ func startupSyncNeedsRetry(err error) bool { // Reconcile resolves one policy and replaces the local routes previously // programmed for that policy. func (r *ServiceRoutePolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + start := time.Now() + resultLabel := "success" + defer func() { r.Metrics.ObserveReconcile(resultLabel, time.Since(start)) }() + logger := log.FromContext(ctx).WithValues("policy", req.NamespacedName.String(), "node", r.NodeName) + r.mu.Lock() defer r.mu.Unlock() policy := &networkv1alpha1.ServiceRoutePolicy{} if err := r.Get(ctx, req.NamespacedName, policy); err != nil { if apierrors.IsNotFound(err) { - return ctrl.Result{}, r.removePolicyLocked(req.NamespacedName) + err := r.removePolicyLocked(req.NamespacedName) + if err != nil { + resultLabel = "error" + logger.Error(err, "remove eBPF policy for deleted ServiceRoutePolicy") + } + return ctrl.Result{}, err } + resultLabel = "error" + logger.Error(err, "get ServiceRoutePolicy") return ctrl.Result{}, err } endpoint := &networkv1alpha1.ServiceEndpoint{} endpointKey := types.NamespacedName{Namespace: policy.Namespace, Name: policy.Spec.ServiceRef.Name} if err := r.Get(ctx, endpointKey, endpoint); err != nil { if apierrors.IsNotFound(err) { - return ctrl.Result{}, errors.Join( + joined := errors.Join( r.replacePolicyLocked(req.NamespacedName, nil), r.setAccepted(ctx, policy, metav1.ConditionFalse, "EndpointNotFound", "referenced ServiceEndpoint does not exist"), ) + if joined != nil { + resultLabel = "error" + logger.Error(joined, "revoke eBPF policy for missing ServiceEndpoint") + } + return ctrl.Result{}, joined } + resultLabel = "error" + logger.Error(err, "get ServiceEndpoint", "endpoint", policy.Spec.ServiceRef.Name) return ctrl.Result{}, fmt.Errorf("get ServiceEndpoint %s/%s: %w", policy.Namespace, policy.Spec.ServiceRef.Name, err) } attachments := &cloudv1alpha1.VPCAttachmentList{} if err := r.List(ctx, attachments); err != nil { + resultLabel = "error" + logger.Error(err, "list VPCAttachments") return ctrl.Result{}, fmt.Errorf("list VPCAttachments: %w", err) } all := make([]*cloudv1alpha1.VPCAttachment, 0, len(attachments.Items)) @@ -121,17 +143,23 @@ func (r *ServiceRoutePolicyReconciler) Reconcile(ctx context.Context, req ctrl.R // uninvolved nodes that correctly compile no local intents. Revoke // this node's stale state and return the error for controller-runtime // to retry while leaving the policy accepted cluster-wide. - return ctrl.Result{}, errors.Join( + joined := errors.Join( err, r.removePolicyLocked(req.NamespacedName), r.setAccepted(ctx, policy, metav1.ConditionTrue, "Accepted", "policy is valid"), ) + resultLabel = "error" + logger.Error(joined, "resolve service routing dependency", "endpoint", endpoint.Name) + return ctrl.Result{}, joined } - return ctrl.Result{}, errors.Join( + joined := errors.Join( err, r.removePolicyLocked(req.NamespacedName), r.setAccepted(ctx, policy, metav1.ConditionFalse, "Invalid", err.Error()), ) + resultLabel = "error" + logger.Error(joined, "compile service eBPF policy", "endpoint", endpoint.Name) + return ctrl.Result{}, joined } if len(intents) != 0 { // The datapath loader may replace an incompatible pinned map while this @@ -139,17 +167,27 @@ func (r *ServiceRoutePolicyReconciler) Reconcile(ctx context.Context, req ctrl.R // change and reconstructs all desired state before an unchanged intent // is otherwise skipped by replacePolicy. if err := r.Programmer.Initialize(); err != nil { + resultLabel = "error" + logger.Error(err, "refresh service route maps", "endpoint", endpoint.Name) return ctrl.Result{}, fmt.Errorf("refresh service route maps: %w", err) } } + logger.Info("compiled service eBPF policy", "endpoint", endpoint.Name, "attachments", len(intents)) if err := r.replacePolicyLocked(req.NamespacedName, intents); err != nil { + resultLabel = "error" + logger.Error(err, "program service eBPF policy", "endpoint", endpoint.Name) + return ctrl.Result{}, err + } + if err := r.setAccepted(ctx, policy, metav1.ConditionTrue, "Accepted", "policy is valid"); err != nil { + resultLabel = "error" + logger.Error(err, "update ServiceRoutePolicy acceptance status") return ctrl.Result{}, err } - result := ctrl.Result{} + requeue := ctrl.Result{} if len(intents) != 0 { - result.RequeueAfter = serviceRouteMapResyncInterval + requeue.RequeueAfter = serviceRouteMapResyncInterval } - return result, r.setAccepted(ctx, policy, metav1.ConditionTrue, "Accepted", "policy is valid") + return requeue, nil } func (r *ServiceRoutePolicyReconciler) setAccepted( @@ -427,6 +465,7 @@ func (r *ServiceRoutePolicyReconciler) replacePolicyLocked( key types.NamespacedName, intents []serviceroute.RouteIntent, ) error { + defer r.updateProgrammedGauge() if r.Applied == nil { r.Applied = make(map[types.NamespacedName]map[types.NamespacedName]serviceroute.RouteIntent) } @@ -449,8 +488,10 @@ func (r *ServiceRoutePolicyReconciler) replacePolicyLocked( continue } if err := r.Programmer.Remove(existing); err != nil { + r.Metrics.ObserveOperation("remove", err) return &serviceRoutePolicyCleanupError{err: err} } + r.Metrics.ObserveOperation("remove", nil) delete(current, attachment) } for attachment, intent := range desired { @@ -458,11 +499,13 @@ func (r *ServiceRoutePolicyReconciler) replacePolicyLocked( continue } if err := r.Programmer.Apply(intent); err != nil { + r.Metrics.ObserveOperation("apply", err) if cleanupErr := r.Programmer.Cleanup(intent); cleanupErr != nil { return errors.Join(err, &serviceRoutePolicyCleanupError{err: cleanupErr}) } return err } + r.Metrics.ObserveOperation("apply", nil) current[attachment] = intent } if len(current) == 0 { @@ -478,13 +521,24 @@ func (r *ServiceRoutePolicyReconciler) removePolicy(key types.NamespacedName) er } func (r *ServiceRoutePolicyReconciler) removePolicyLocked(key types.NamespacedName) error { + defer r.updateProgrammedGauge() current := r.Applied[key] for attachment, intent := range current { if err := r.Programmer.Remove(intent); err != nil { + r.Metrics.ObserveOperation("remove", err) return &serviceRoutePolicyCleanupError{err: err} } + r.Metrics.ObserveOperation("remove", nil) delete(current, attachment) } delete(r.Applied, key) return nil } + +func (r *ServiceRoutePolicyReconciler) updateProgrammedGauge() { + count := 0 + for _, intents := range r.Applied { + count += len(intents) + } + r.Metrics.SetProgrammedRoutes(count) +} diff --git a/internal/serviceroute/metrics.go b/internal/serviceroute/metrics.go new file mode 100644 index 00000000..609456b3 --- /dev/null +++ b/internal/serviceroute/metrics.go @@ -0,0 +1,82 @@ +// Copyright 2026 Datum Cloud, Inc. +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +package serviceroute + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +const metricsNamespace = "galactic_service_route" + +// Metrics contains bounded-cardinality metrics for the node-local service +// route controller. Resource names are deliberately kept in logs instead of +// labels so a large tenant population does not create an unbounded registry. +type Metrics struct { + reconciliations *prometheus.CounterVec + reconcileTime prometheus.Histogram + operations *prometheus.CounterVec + programmed prometheus.Gauge +} + +// NewMetrics creates the service-route metrics. Call MustRegister once during +// process startup. +func NewMetrics() *Metrics { + reconcileTime := prometheus.NewHistogram(prometheus.HistogramOpts{ + Namespace: metricsNamespace, + Name: "reconcile_duration_seconds", + Help: "Time spent reconciling a ServiceRoutePolicy.", + Buckets: prometheus.DefBuckets, + }) + return &Metrics{ + reconciliations: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: metricsNamespace, + Name: "reconciliations_total", + Help: "ServiceRoutePolicy reconciliations by outcome.", + }, []string{"result"}), + reconcileTime: reconcileTime, + operations: prometheus.NewCounterVec(prometheus.CounterOpts{ + Namespace: metricsNamespace, + Name: "route_operations_total", + Help: "Service route dataplane operations by operation and outcome.", + }, []string{"operation", "result"}), + programmed: prometheus.NewGauge(prometheus.GaugeOpts{ + Namespace: metricsNamespace, + Name: "programmed_routes", + Help: "Number of attachment-scoped eBPF service paths currently tracked by the controller.", + }), + } +} + +// MustRegister registers all metrics owned by this package. +func (m *Metrics) MustRegister(reg prometheus.Registerer) { + reg.MustRegister(m.reconciliations, m.reconcileTime, m.operations, m.programmed) +} + +func (m *Metrics) ObserveReconcile(result string, duration time.Duration) { + if m == nil { + return + } + m.reconciliations.WithLabelValues(result).Inc() + m.reconcileTime.Observe(duration.Seconds()) +} + +func (m *Metrics) ObserveOperation(operation string, err error) { + if m == nil { + return + } + result := "success" + if err != nil { + result = "error" + } + m.operations.WithLabelValues(operation, result).Inc() +} + +func (m *Metrics) SetProgrammedRoutes(count int) { + if m != nil { + m.programmed.Set(float64(count)) + } +} From 37e7f53f35a3ceed2b5753acf78e31ab80eaed45 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 6 Oct 2026 12:12:51 -0500 Subject: [PATCH 2/4] feat(serviceroute): expose eBPF policy drops --- .../controller/serviceroute_controller.go | 22 ++++++++++--------- internal/plumbing/ebpf/metrics/collector.go | 2 +- .../plumbing/ebpf/metrics/collector_test.go | 14 +++++++----- internal/plumbing/ebpf/prog/dropreason.go | 15 +++++++++++++ 4 files changed, 36 insertions(+), 17 deletions(-) diff --git a/internal/controller/serviceroute_controller.go b/internal/controller/serviceroute_controller.go index 3faa82e4..ca871eec 100644 --- a/internal/controller/serviceroute_controller.go +++ b/internal/controller/serviceroute_controller.go @@ -33,6 +33,8 @@ import ( networkv1alpha1 "go.datum.net/network/api/v1alpha1" ) +const reconcileResultError = "error" + // ServiceRoutePolicyReconciler resolves service policies against local Cloud // API attachments and programs the resulting stateful eBPF forwarding policy // directly on this node. It intentionally does not create child Kubernetes @@ -97,12 +99,12 @@ func (r *ServiceRoutePolicyReconciler) Reconcile(ctx context.Context, req ctrl.R if apierrors.IsNotFound(err) { err := r.removePolicyLocked(req.NamespacedName) if err != nil { - resultLabel = "error" + resultLabel = reconcileResultError logger.Error(err, "remove eBPF policy for deleted ServiceRoutePolicy") } return ctrl.Result{}, err } - resultLabel = "error" + resultLabel = reconcileResultError logger.Error(err, "get ServiceRoutePolicy") return ctrl.Result{}, err } @@ -115,18 +117,18 @@ func (r *ServiceRoutePolicyReconciler) Reconcile(ctx context.Context, req ctrl.R r.setAccepted(ctx, policy, metav1.ConditionFalse, "EndpointNotFound", "referenced ServiceEndpoint does not exist"), ) if joined != nil { - resultLabel = "error" + resultLabel = reconcileResultError logger.Error(joined, "revoke eBPF policy for missing ServiceEndpoint") } return ctrl.Result{}, joined } - resultLabel = "error" + resultLabel = reconcileResultError logger.Error(err, "get ServiceEndpoint", "endpoint", policy.Spec.ServiceRef.Name) return ctrl.Result{}, fmt.Errorf("get ServiceEndpoint %s/%s: %w", policy.Namespace, policy.Spec.ServiceRef.Name, err) } attachments := &cloudv1alpha1.VPCAttachmentList{} if err := r.List(ctx, attachments); err != nil { - resultLabel = "error" + resultLabel = reconcileResultError logger.Error(err, "list VPCAttachments") return ctrl.Result{}, fmt.Errorf("list VPCAttachments: %w", err) } @@ -148,7 +150,7 @@ func (r *ServiceRoutePolicyReconciler) Reconcile(ctx context.Context, req ctrl.R r.removePolicyLocked(req.NamespacedName), r.setAccepted(ctx, policy, metav1.ConditionTrue, "Accepted", "policy is valid"), ) - resultLabel = "error" + resultLabel = reconcileResultError logger.Error(joined, "resolve service routing dependency", "endpoint", endpoint.Name) return ctrl.Result{}, joined } @@ -157,7 +159,7 @@ func (r *ServiceRoutePolicyReconciler) Reconcile(ctx context.Context, req ctrl.R r.removePolicyLocked(req.NamespacedName), r.setAccepted(ctx, policy, metav1.ConditionFalse, "Invalid", err.Error()), ) - resultLabel = "error" + resultLabel = reconcileResultError logger.Error(joined, "compile service eBPF policy", "endpoint", endpoint.Name) return ctrl.Result{}, joined } @@ -167,19 +169,19 @@ func (r *ServiceRoutePolicyReconciler) Reconcile(ctx context.Context, req ctrl.R // change and reconstructs all desired state before an unchanged intent // is otherwise skipped by replacePolicy. if err := r.Programmer.Initialize(); err != nil { - resultLabel = "error" + resultLabel = reconcileResultError logger.Error(err, "refresh service route maps", "endpoint", endpoint.Name) return ctrl.Result{}, fmt.Errorf("refresh service route maps: %w", err) } } logger.Info("compiled service eBPF policy", "endpoint", endpoint.Name, "attachments", len(intents)) if err := r.replacePolicyLocked(req.NamespacedName, intents); err != nil { - resultLabel = "error" + resultLabel = reconcileResultError logger.Error(err, "program service eBPF policy", "endpoint", endpoint.Name) return ctrl.Result{}, err } if err := r.setAccepted(ctx, policy, metav1.ConditionTrue, "Accepted", "policy is valid"); err != nil { - resultLabel = "error" + resultLabel = reconcileResultError logger.Error(err, "update ServiceRoutePolicy acceptance status") return ctrl.Result{}, err } diff --git a/internal/plumbing/ebpf/metrics/collector.go b/internal/plumbing/ebpf/metrics/collector.go index d027548b..3d907296 100644 --- a/internal/plumbing/ebpf/metrics/collector.go +++ b/internal/plumbing/ebpf/metrics/collector.go @@ -324,7 +324,7 @@ func (c *Collector) collectDrops(ch chan<- prometheus.Metric) { if c.dropReasons == nil { return } - for i := range prog.DropReasonCount { + for _, i := range prog.DropReasonMetricIndices { var perCPU []uint64 if err := c.dropReasons.Lookup(i, &perCPU); err != nil { ch <- prometheus.NewInvalidMetric(dropsDesc, fmt.Errorf("lookup drop_reasons[%d]: %w", i, err)) diff --git a/internal/plumbing/ebpf/metrics/collector_test.go b/internal/plumbing/ebpf/metrics/collector_test.go index 162f5175..2d9a75ea 100644 --- a/internal/plumbing/ebpf/metrics/collector_test.go +++ b/internal/plumbing/ebpf/metrics/collector_test.go @@ -286,8 +286,9 @@ func findBlockGauges(metrics []*dto.Metric, block uint64) (used, ratio float64, func TestCollector_Drops(t *testing.T) { drops := fakeDropReasons{ - prog.DropReasonUnknownArgument: 42, - prog.DropReasonFibLookupFailed: 7, + prog.DropReasonUnknownArgument: 42, + prog.DropReasonFibLookupFailed: 7, + prog.DropReasonServiceRouteDenied: 3, } c := NewCollector(usidmap.NewVRFTable(newFakeTable()), usidmap.NewLocatorTable(newFakeTable()), drops, nil) @@ -308,20 +309,21 @@ func TestCollector_Drops(t *testing.T) { "strip_failed": 0, "fib_lookup_failed": 7, "redirect_failed": 0, + "service_route_denied": 3, } for reason, wantVal := range want { got, ok := seen[reason] if !ok { - t.Errorf("reason %q not emitted at all (all %d drop reasons must always be emitted, even at zero)", - reason, prog.DropReasonCount) + t.Errorf("reason %q not emitted at all (all %d metric reasons must always be emitted, even at zero)", + reason, len(prog.DropReasonMetricIndices)) continue } if got != wantVal { t.Errorf("reason %q = %v, want %v", reason, got, wantVal) } } - if len(seen) != int(prog.DropReasonCount) { - t.Errorf("emitted %d distinct drop reasons, want %d", len(seen), prog.DropReasonCount) + if len(seen) != len(prog.DropReasonMetricIndices) { + t.Errorf("emitted %d distinct drop reasons, want %d", len(seen), len(prog.DropReasonMetricIndices)) } } diff --git a/internal/plumbing/ebpf/prog/dropreason.go b/internal/plumbing/ebpf/prog/dropreason.go index db4e7db2..ae964fc3 100644 --- a/internal/plumbing/ebpf/prog/dropreason.go +++ b/internal/plumbing/ebpf/prog/dropreason.go @@ -30,8 +30,21 @@ const ( DropReasonEgressRouteRedirectFailed uint32 = 14 DropReasonPublicUplinkRedirectFailed uint32 = 15 DropReasonCount uint32 = 16 + DropReasonServiceRouteDenied uint32 = 40 + DropReasonServiceRouteRedirectFailed uint32 = 41 ) +// DropReasonMetricIndices lists stable production counters. Diagnostic trace +// slots between DropReasonCount and the service-route counters are deliberately +// excluded from metrics. +var DropReasonMetricIndices = func() []uint32 { + indices := make([]uint32, 0, DropReasonCount+2) + for index := range DropReasonCount { + indices = append(indices, index) + } + return append(indices, DropReasonServiceRouteDenied, DropReasonServiceRouteRedirectFailed) +}() + // DropReasonNames maps each index to a short, stable, metrics-friendly name, // decoupling label values and any other external representation from the C // identifier spelling. @@ -52,4 +65,6 @@ var DropReasonNames = map[uint32]string{ DropReasonEgressRouteFibLookupFailed: "egress_route_fib_lookup_failed", DropReasonEgressRouteRedirectFailed: "egress_route_redirect_failed", DropReasonPublicUplinkRedirectFailed: "public_uplink_redirect_failed", + DropReasonServiceRouteDenied: "service_route_denied", + DropReasonServiceRouteRedirectFailed: "service_route_redirect_failed", } From 725e89730456e8811e3e3e46a7749230652ea8db Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 6 Oct 2026 13:22:26 -0500 Subject: [PATCH 3/4] fix(monitoring): scrape CNI eBPF metrics --- config/monitoring/kustomization.yaml | 1 + .../monitoring/podmonitor-galactic-cni.yaml | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 config/monitoring/podmonitor-galactic-cni.yaml diff --git a/config/monitoring/kustomization.yaml b/config/monitoring/kustomization.yaml index 8a22f7a7..0cc73ced 100644 --- a/config/monitoring/kustomization.yaml +++ b/config/monitoring/kustomization.yaml @@ -14,6 +14,7 @@ namespace: galactic-system resources: - podmonitor-fabric-router.yaml + - podmonitor-galactic-cni.yaml - podmonitor-galactic-nat.yaml - podmonitor-galactic-router.yaml - prometheusrule.yaml diff --git a/config/monitoring/podmonitor-galactic-cni.yaml b/config/monitoring/podmonitor-galactic-cni.yaml new file mode 100644 index 00000000..4647699a --- /dev/null +++ b/config/monitoring/podmonitor-galactic-cni.yaml @@ -0,0 +1,19 @@ +# Scrapes galactic-cni's per-node metrics port (metrics, 9180), including the +# TC-eBPF service policy and drop counters. +apiVersion: monitoring.coreos.com/v1 +kind: PodMonitor +metadata: + name: galactic-cni + labels: + app.kubernetes.io/name: galactic-cni +spec: + jobLabel: app.kubernetes.io/name + selector: + matchLabels: + app.kubernetes.io/name: galactic-cni + podMetricsEndpoints: + - port: metrics + interval: 30s + relabelings: + - sourceLabels: [__meta_kubernetes_pod_node_name] + targetLabel: node From f10c9faa38d363460eb2073eb207e02cefa1a425 Mon Sep 17 00:00:00 2001 From: Scot Wells Date: Tue, 6 Oct 2026 13:32:03 -0500 Subject: [PATCH 4/4] fix observability lint --- internal/controller/serviceroute_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/controller/serviceroute_controller.go b/internal/controller/serviceroute_controller.go index ca871eec..86d5d802 100644 --- a/internal/controller/serviceroute_controller.go +++ b/internal/controller/serviceroute_controller.go @@ -89,7 +89,7 @@ func (r *ServiceRoutePolicyReconciler) Reconcile(ctx context.Context, req ctrl.R start := time.Now() resultLabel := "success" defer func() { r.Metrics.ObserveReconcile(resultLabel, time.Since(start)) }() - logger := log.FromContext(ctx).WithValues("policy", req.NamespacedName.String(), "node", r.NodeName) + logger := log.FromContext(ctx).WithValues("policy", req.String(), "node", r.NodeName) r.mu.Lock() defer r.mu.Unlock()