Skip to content

Commit d2fe202

Browse files
committed
exporter/otlp: fix User-Agent header being ignored in gRPC exporter
Signed-off-by: 57Ajay <57ajay.u@gmail.com>
1 parent 1cf90bc commit d2fe202

4 files changed

Lines changed: 87 additions & 0 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
change_type: enhancement
2+
component: pkg/config/configgrpc
3+
note: Add `UserAgent` field to `ClientConfig` to allow overriding the default gRPC user-agent string.
4+
issues: [14686]
5+
subtext: |
6+
The otlp gRPC exporter was unconditionally setting the User-Agent via
7+
grpc.WithUserAgent() at dial time, which takes precedence over per-call
8+
metadata, causing any user-configured User-Agent to be silently discarded.
9+
A dedicated `UserAgent` field has been added to `ClientConfig` which, when
10+
set, is used in the dial option directly instead of the default BuildInfo-derived string.

config/configgrpc/configgrpc.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,11 @@ type ClientConfig struct {
100100
// The headers associated with gRPC requests.
101101
Headers configopaque.MapList `mapstructure:"headers,omitempty"`
102102

103+
// UserAgent overrides the default user-agent header sent on gRPC requests.
104+
// The default is derived from the build info. When empty, the caller controls
105+
// the user-agent via grpc.WithUserAgent or similar options.
106+
UserAgent string `mapstructure:"user_agent,omitempty"`
107+
103108
// Sets the balancer in grpclb_policy to discover the servers. Default is pick_first.
104109
// https://github.com/grpc/grpc-go/blob/master/examples/features/load_balancing/README.md
105110
BalancerName string `mapstructure:"balancer_name"`
@@ -441,6 +446,10 @@ func (cc *ClientConfig) getGrpcDialOptions(
441446
}
442447
}
443448

449+
if cc.UserAgent != "" {
450+
opts = append(opts, grpc.WithUserAgent(cc.UserAgent))
451+
}
452+
444453
return opts, nil
445454
}
446455

config/configgrpc/configgrpc_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1319,3 +1319,25 @@ func tempSocketName(t *testing.T) string {
13191319
require.NoError(t, os.Remove(socket))
13201320
return socket
13211321
}
1322+
1323+
func TestGrpcClientUserAgent(t *testing.T) {
1324+
cc := &ClientConfig{
1325+
TLS: configtls.ClientConfig{
1326+
Insecure: true,
1327+
},
1328+
UserAgent: "my-distribution/1.0",
1329+
}
1330+
opts, err := cc.getGrpcDialOptions(
1331+
context.Background(),
1332+
nil,
1333+
componenttest.NewNopTelemetrySettings(),
1334+
[]ToClientConnOption{},
1335+
)
1336+
require.NoError(t, err)
1337+
/* Expecting 3 DialOptions:
1338+
* - WithTransportCredentials (TLS)
1339+
* - WithStatsHandler (always, for self-telemetry)
1340+
* - WithUserAgent (from UserAgent field)
1341+
*/
1342+
assert.Len(t, opts, 3)
1343+
}

exporter/otlpexporter/otlp_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net"
99
"path/filepath"
1010
"runtime"
11+
"strings"
1112
"sync"
1213
"sync/atomic"
1314
"testing"
@@ -1042,3 +1043,48 @@ func TestSendProfilesWhenEndpointHasHttpScheme(t *testing.T) {
10421043
})
10431044
}
10441045
}
1046+
1047+
func TestUserAgentHeader_OverriddenByConfig(t *testing.T) {
1048+
// Start an OTLP-compatible receiver.
1049+
ln, err := net.Listen("tcp", "localhost:")
1050+
require.NoError(t, err, "Failed to find an available address to run the gRPC server: %v", err)
1051+
rcv := otlpMetricsReceiverOnGRPCServer(ln)
1052+
defer rcv.srv.GracefulStop()
1053+
1054+
// Start an OTLP exporter with a custom User-Agent header.
1055+
factory := NewFactory()
1056+
cfg := factory.CreateDefaultConfig().(*Config)
1057+
cfg.QueueConfig = configoptional.None[exporterhelper.QueueBatchConfig]()
1058+
cfg.ClientConfig = configgrpc.ClientConfig{
1059+
Endpoint: ln.Addr().String(),
1060+
TLS: configtls.ClientConfig{
1061+
Insecure: true,
1062+
},
1063+
UserAgent: "My Distribution For The Collector",
1064+
}
1065+
set := exportertest.NewNopSettings(factory.Type())
1066+
set.BuildInfo.Description = "Collector"
1067+
set.BuildInfo.Version = "1.2.3test"
1068+
1069+
exp, err := factory.CreateMetrics(context.Background(), set, cfg)
1070+
require.NoError(t, err)
1071+
require.NotNil(t, exp)
1072+
defer func() {
1073+
assert.NoError(t, exp.Shutdown(context.Background()))
1074+
}()
1075+
1076+
require.NoError(t, exp.Start(context.Background(), componenttest.NewNopHost()))
1077+
1078+
// Send a metric to trigger an RPC.
1079+
require.NoError(t, exp.ConsumeMetrics(context.Background(), pmetric.NewMetrics()))
1080+
1081+
assert.Eventually(t, func() bool {
1082+
return rcv.requestCount.Load() > 0
1083+
}, 10*time.Second, 5*time.Millisecond)
1084+
1085+
md := rcv.getMetadata()
1086+
require.Len(t, md.Get("User-Agent"), 1)
1087+
// User-configured value must win over the builtin BuildInfo agent.
1088+
require.True(t, strings.HasPrefix(md.Get("User-Agent")[0], "My Distribution For The Collector"))
1089+
require.NotContains(t, md.Get("User-Agent")[0], "Collector/1.2.3test")
1090+
}

0 commit comments

Comments
 (0)