Skip to content

Commit 82f2e87

Browse files
ajimenez1503antonjim-teaxw
authored
[exporter/loadbalancing] Stabilize attribute routing keys and non-string value handling (#46420)
#### Description - Implement stable attribute routing key encoding for loadbalancing exporter as `name=value|` segments with explicit missing markers (`name=|`) across traces and metrics. - Centralize attribute value conversion in `attribute_routing.go` so routing key stringification logic lives in one place and is reused by both signals. - Add/update tests for: - collision avoidance with segmented keys - non-string attribute routing behavior - deterministic map/slice stringification behavior - Update docs/changelog to document the routing key format and non-string handling. <!-- Issue number (e.g. #1234) or full URL to issue, if applicable. --> #### Link to tracking issue Resolve #46094 Resolve #46095 <!--Describe what testing was performed and which tests were added.--> #### Testing Added unit test for attribute routing. #### Documentation Updated `exporter/loadbalancingexporter/README.md` to describe attribute routing logic. --------- Co-authored-by: Antonio Jimenez <antonjim@thousandEyes.com> Co-authored-by: Andrew Wilkins <axw@elastic.co>
1 parent 4e3dacc commit 82f2e87

19 files changed

Lines changed: 294 additions & 41 deletions

File tree

.chloggen/feat_46094_46095.yaml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Use this changelog template to create an entry for release notes.
2+
3+
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
4+
change_type: "enhancement"
5+
6+
# The name of the component, or a single word describing the area of concern, (e.g. receiver/filelog)
7+
component: "exporter/loadbalancing"
8+
9+
# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
10+
note: "Add stable attribute routing key encoding for traces and metrics in the loadbalancing exporter"
11+
12+
# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists.
13+
issues: [46094, 46095]
14+
15+
# (Optional) One or more lines of additional information to render under the primary note.
16+
# These lines will be padded with 2 spaces and then inserted directly into the document.
17+
# Use pipe (|) for multiline entries.
18+
subtext: |
19+
Routing keys now encode attributes as `name=value|` segments, including explicit markers for missing attributes.
20+
Non-string attribute values are deterministically stringified and used consistently across traces and metrics.
21+
22+
# If your change doesn't affect end users or the exported elements of any package,
23+
# you should instead start your pull request title with [chore] or use the "Skip Changelog" label.
24+
# Optional: The change log or logs in which this entry should be included.
25+
# e.g. '[user]' or '[user, api]'
26+
# Include 'user' if the change is relevant to end users.
27+
# Include 'api' if there is a change to a library API.
28+
# Default: '[user]'
29+
change_logs: [user]

exporter/loadbalancingexporter/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ Refer to [config.yaml](./testdata/config.yaml) for detailed examples on using th
124124
* `metric`: Routes metrics based on their metric name. Invalid for spans.
125125
* `streamID`: Routes metrics based on their datapoint streamID. That's the unique hash of all it's attributes, plus the attributes and identifying information of its resource, scope, and metric data.
126126
* loadbalancing exporter supports set of standard [queuing, retry and timeout settings](https://github.com/open-telemetry/opentelemetry-collector/blob/main/exporter/exporterhelper/README.md), but they are disable by default to maintain compatibility
127-
* The `routing_attributes` property is used to list the attributes that should be used if the `routing_key` is `attributes`.
127+
* The `routing_attributes` property is used to list the attributes that should be used if the `routing_key` is `attributes`. For both traces and metrics, keys are encoded in configured order as `name=value|name=value|`, and missing attributes are encoded as `name=|`. Non-string values are deterministically stringified.
128128

129129
Simple example
130130

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package loadbalancingexporter // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/loadbalancingexporter"
5+
6+
import (
7+
"go.opentelemetry.io/collector/pdata/pcommon"
8+
)
9+
10+
// buildAttributeRoutingKey encodes a missing attribute as "name=|".
11+
func buildAttributeRoutingKey(attr string) string {
12+
return attr + "=|"
13+
}
14+
15+
// buildAttributeRoutingKeyStrValue encodes a single string attribute key/value pair as
16+
// "name=value|".
17+
func buildAttributeRoutingKeyStrValue(attr, value string) string {
18+
return attr + "=" + value + "|"
19+
}
20+
21+
// buildAttributeRoutingKeyValue encodes a single attribute key/value pair as
22+
// "name=value|".
23+
func buildAttributeRoutingKeyValue(attr string, value pcommon.Value) string {
24+
return attr + "=" + value.AsString() + "|"
25+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package loadbalancingexporter
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"go.opentelemetry.io/collector/pdata/pcommon"
11+
)
12+
13+
func TestBuildAttributeRoutingKeyValue_Primitives(t *testing.T) {
14+
tests := []struct {
15+
name string
16+
value pcommon.Value
17+
want string
18+
}{
19+
{name: "string", value: pcommon.NewValueStr("abc"), want: "k=abc|"},
20+
{name: "int", value: pcommon.NewValueInt(42), want: "k=42|"},
21+
{name: "double", value: pcommon.NewValueDouble(3.5), want: "k=3.5|"},
22+
{name: "bool", value: pcommon.NewValueBool(true), want: "k=true|"},
23+
{name: "empty", value: pcommon.NewValueEmpty(), want: "k=|"},
24+
{
25+
name: "bytes",
26+
value: func() pcommon.Value {
27+
v := pcommon.NewValueBytes()
28+
v.Bytes().FromRaw([]byte{0x01, 0xFF})
29+
return v
30+
}(),
31+
want: "k=Af8=|",
32+
},
33+
}
34+
35+
for _, tc := range tests {
36+
t.Run(tc.name, func(t *testing.T) {
37+
assert.Equal(t, tc.want, buildAttributeRoutingKeyValue("k", tc.value))
38+
})
39+
}
40+
}
41+
42+
func TestBuildAttributeRoutingKeyValue_MapAndSliceDeterministic(t *testing.T) {
43+
m1 := pcommon.NewValueMap()
44+
m1.Map().PutInt("b", 2)
45+
m1.Map().PutStr("a", "x")
46+
47+
m2 := pcommon.NewValueMap()
48+
m2.Map().PutStr("a", "x")
49+
m2.Map().PutInt("b", 2)
50+
51+
assert.Equal(t, "k={\"a\":\"x\",\"b\":2}|", buildAttributeRoutingKeyValue("k", m1))
52+
assert.Equal(t, buildAttributeRoutingKeyValue("k", m1), buildAttributeRoutingKeyValue("k", m2))
53+
54+
s := pcommon.NewValueSlice()
55+
s.Slice().AppendEmpty().SetInt(1)
56+
s.Slice().AppendEmpty().SetStr("x")
57+
s.Slice().AppendEmpty().SetBool(false)
58+
59+
assert.Equal(t, "k=[1,\"x\",false]|", buildAttributeRoutingKeyValue("k", s))
60+
}

exporter/loadbalancingexporter/config.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,8 @@ type Config struct {
5151
// For traces, attributes can come from resource, scope, or span, plus the pseudo attributes "span.kind" and
5252
// "span.name".
5353
// For metrics, attributes can come from resource, scope, or datapoint attributes.
54+
// Keys are encoded as "name=value|name=value|" in the order configured. Missing attributes are encoded as "name=|".
55+
// Non-string values are deterministically stringified.
5456
RoutingAttributes []string `mapstructure:"routing_attributes"`
5557
}
5658

exporter/loadbalancingexporter/config.schema.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ properties:
8686
resolver:
8787
$ref: resolver_settings
8888
routing_attributes:
89-
description: RoutingAttributes creates a composite routing key from the listed attributes. For traces, attributes can come from resource, scope, or span, plus the pseudo attributes "span.kind" and "span.name". For metrics, attributes can come from resource, scope, or datapoint attributes.
89+
description: RoutingAttributes creates a composite routing key from the listed attributes. For traces, attributes can come from resource, scope, or span, plus the pseudo attributes "span.kind" and "span.name". For metrics, attributes can come from resource, scope, or datapoint attributes. Keys are encoded as "name=value|name=value|" in the order configured. Missing attributes are encoded as "name=|". Non-string values are deterministically stringified.
9090
type: array
9191
items:
9292
type: string

exporter/loadbalancingexporter/generated_package_test.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

exporter/loadbalancingexporter/loadbalancer_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package loadbalancingexporter
66
import (
77
"context"
88
"errors"
9+
"net"
910
"testing"
1011

1112
"github.com/stretchr/testify/assert"
@@ -145,6 +146,14 @@ func TestWithDNSResolverNoEndpoints(t *testing.T) {
145146
require.NotNil(t, p)
146147
require.NoError(t, err)
147148

149+
dnsRes, ok := p.res.(*dnsResolver)
150+
require.True(t, ok)
151+
dnsRes.resolver = &mockDNSResolver{
152+
onLookupIPAddr: func(context.Context, string) ([]net.IPAddr, error) {
153+
return nil, nil
154+
},
155+
}
156+
148157
err = p.Start(t.Context(), componenttest.NewNopHost())
149158
require.NoError(t, err)
150159
defer func() { assert.NoError(t, p.Shutdown(t.Context())) }()

exporter/loadbalancingexporter/log_exporter_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ func TestLogExporterStart(t *testing.T) {
7373
"ok",
7474
func() *logExporterImp {
7575
p, _ := newLogsExporter(exportertest.NewNopSettings(metadata.Type), simpleConfig())
76+
p.loadBalancer.res = &mockResolver{}
7677
return p
7778
}(),
7879
nil,

exporter/loadbalancingexporter/metadata.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,4 +96,5 @@ tests:
9696
# Ignore goroutines spawned by net.Resolver for DNS lookups on Windows
9797
# These can remain in syscall state during test cleanup
9898
- internal/poll.runtime_pollWait
99+
- syscall.SyscallN
99100
- syscall.Syscall6

0 commit comments

Comments
 (0)