From 502eed087de9b09913f5d4ad11f19519a63ce9e9 Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Tue, 25 Nov 2025 13:39:53 +0100 Subject: [PATCH 1/2] Fix k8s resolver parsing so loadbalancing exporter works with service FQDNs Signed-off-by: Israel Blancas --- .chloggen/44472.yaml | 27 +++++++++++ .../loadbalancingexporter/resolver_k8s.go | 8 ++-- .../resolver_k8s_test.go | 48 +++++++++++++++++++ 3 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 .chloggen/44472.yaml diff --git a/.chloggen/44472.yaml b/.chloggen/44472.yaml new file mode 100644 index 0000000000000..3ae404ea04d20 --- /dev/null +++ b/.chloggen/44472.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: bug_fix + +# The name of the component, or a single word describing the area of concern, (e.g. receiver/filelog) +component: exporter/loadbalancing + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: "Fix k8s resolver parsing so loadbalancing exporter works with service FQDNs" + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [44472] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/exporter/loadbalancingexporter/resolver_k8s.go b/exporter/loadbalancingexporter/resolver_k8s.go index e3fdb98081256..d588ff93ee049 100644 --- a/exporter/loadbalancingexporter/resolver_k8s.go +++ b/exporter/loadbalancingexporter/resolver_k8s.go @@ -87,10 +87,10 @@ func newK8sResolver(clt kubernetes.Interface, timeout = defaultListWatchTimeout } - nAddr := strings.SplitN(service, ".", 2) - name, namespace := nAddr[0], "default" - if len(nAddr) > 1 { - namespace = nAddr[1] + parts := strings.Split(service, ".") + name, namespace := parts[0], "default" + if len(parts) > 1 && parts[1] != "" { + namespace = parts[1] } else { logger.Info("the namespace for the Kubernetes service wasn't provided, trying to determine the current namespace", zap.String("name", name)) if ns, err := getInClusterNamespace(); err == nil { diff --git a/exporter/loadbalancingexporter/resolver_k8s_test.go b/exporter/loadbalancingexporter/resolver_k8s_test.go index 423f05b530f27..4d0c94e94e555 100644 --- a/exporter/loadbalancingexporter/resolver_k8s_test.go +++ b/exporter/loadbalancingexporter/resolver_k8s_test.go @@ -257,6 +257,54 @@ func TestK8sResolve(t *testing.T) { } } +func TestK8sResolveWithServiceFQDN(t *testing.T) { + serviceName := "lb" + namespace := "custom" + serviceFQDN := fmt.Sprintf("%s.%s.svc.cluster.local", serviceName, namespace) + port := int32(4317) + hostname := "pod-0" + + endpointSlice := &discoveryv1.EndpointSlice{ + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Namespace: namespace, + Labels: map[string]string{ + "kubernetes.io/service-name": serviceName, + }, + }, + Endpoints: []discoveryv1.Endpoint{ + { + Addresses: []string{"10.0.0.1"}, + Hostname: &hostname, + }, + }, + } + + cl := fake.NewClientset(endpointSlice) + _, tb := getTelemetryAssets(t) + res, err := newK8sResolver(cl, zap.NewNop(), serviceFQDN, []int32{port}, defaultListWatchTimeout, true, tb) + require.NoError(t, err) + require.Equal(t, serviceName, res.svcName) + require.Equal(t, namespace, res.svcNs) + + require.NoError(t, res.start(t.Context())) + t.Cleanup(func() { + require.NoError(t, res.shutdown(t.Context())) + }) + + expected := []string{fmt.Sprintf("%s.%s.%s:%d", hostname, serviceName, namespace, port)} + + cErr := waitForCondition(t, 3*time.Second, 20*time.Millisecond, func(ctx context.Context) (bool, error) { + if _, err := res.resolve(ctx); err != nil { + return false, err + } + return slices.Equal(expected, res.Endpoints()), nil + }) + if cErr != nil { + t.Fatalf("timed out waiting for resolver endpoints to match expected: %v", cErr) + } +} + // waitForCondition will poll the condition function until it returns true or times out. // Any errors returned from the condition are treated as test failures. func waitForCondition(t *testing.T, timeout, interval time.Duration, condition func(context.Context) (bool, error)) error { From 53336683c8050346c6a2ff762c231673a55050e8 Mon Sep 17 00:00:00 2001 From: Israel Blancas Date: Tue, 25 Nov 2025 16:49:50 +0100 Subject: [PATCH 2/2] Add note about new RBAC needed Signed-off-by: Israel Blancas --- exporter/loadbalancingexporter/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/exporter/loadbalancingexporter/README.md b/exporter/loadbalancingexporter/README.md index 9a8b9bab21548..fa29598d004b3 100644 --- a/exporter/loadbalancingexporter/README.md +++ b/exporter/loadbalancingexporter/README.md @@ -100,6 +100,7 @@ Refer to [config.yaml](./testdata/config.yaml) for detailed examples on using th * `ports` port to be used for exporting the traces to the addresses resolved from `service`. If `ports` is not specified, the default port 4317 is used. When multiple ports are specified, two backends are added to the load balancer as if they were at different pods. * `timeout` resolver timeout in go-Duration format, e.g. `5s`, `1d`, `30m`. If not specified, `1s` will be used. * `return_hostnames` will return hostnames instead of IPs. This is useful in certain situations like using istio in sidecar mode. To use this feature, the `service` must be a headless `Service`, pointing at a `StatefulSet`, and the `service` must be what is specified under `.spec.serviceName` in the `StatefulSet`. + * **RBAC requirement:** the Collector pod must run with a service account that is allowed to `get`, `list`, and `watch` `discovery.k8s.io/v1` `EndpointSlice` objects in the target namespace; otherwise the resolver cache remains empty and the exporter logs `couldn't find the exporter for the endpoint ""`. * The `aws_cloud_map` node accepts the following properties: * `namespace` The CloudMap namespace where the service is register, e.g. `cloudmap`. If no `namespace` is specified, this will fail to start the Load Balancer exporter. * `service_name` The name of the service that you specified when you registered the instance, e.g. `otelcollectors`. If no `service_name` is specified, this will fail to start the Load Balancer exporter.