Skip to content

Commit 28b6e34

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 28b6e34

4 files changed

Lines changed: 65 additions & 2 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: 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: 5 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"`

exporter/otlpexporter/otlp.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,6 @@ import (
3434
type baseExporter struct {
3535
// Input configuration.
3636
config *Config
37-
3837
// gRPC clients and connection.
3938
traceExporter ptraceotlp.GRPCClient
4039
metricExporter pmetricotlp.GRPCClient
@@ -62,7 +61,11 @@ func newExporter(cfg component.Config, set exporter.Settings) *baseExporter {
6261
// start actually creates the gRPC connection. The client construction is deferred till this point as this
6362
// is the only place we get hold of Extensions which are required to construct auth round tripper.
6463
func (e *baseExporter) start(ctx context.Context, host component.Host) (err error) {
65-
agentOpt := configgrpc.WithGrpcDialOption(grpc.WithUserAgent(e.userAgent))
64+
userAgent := e.config.ClientConfig.UserAgent
65+
if userAgent == "" {
66+
userAgent = e.userAgent
67+
}
68+
agentOpt := configgrpc.WithGrpcDialOption(grpc.WithUserAgent(userAgent))
6669
if e.clientConn, err = e.config.ClientConfig.ToClientConn(ctx, host.GetExtensions(), e.settings, agentOpt); err != nil {
6770
return err
6871
}

exporter/otlpexporter/otlp_test.go

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

0 commit comments

Comments
 (0)