Skip to content

Commit faeadba

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 faeadba

3 files changed

Lines changed: 70 additions & 6 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
change_type: bug_fix
2+
component: exporter/otlp
3+
note: Fix User-Agent header being ignored when configured in the gRPC exporter headers.
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. A user-configured User-Agent header was silently discarded.
9+
The fix checks for a user-supplied User-Agent in the headers config and
10+
uses it in the dial option directly.

exporter/otlpexporter/otlp.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"context"
88
"fmt"
99
"runtime"
10+
"strings"
1011

1112
"go.uber.org/zap"
1213
"google.golang.org/genproto/googleapis/rpc/errdetails"
@@ -34,7 +35,6 @@ import (
3435
type baseExporter struct {
3536
// Input configuration.
3637
config *Config
37-
3838
// gRPC clients and connection.
3939
traceExporter ptraceotlp.GRPCClient
4040
metricExporter pmetricotlp.GRPCClient
@@ -62,18 +62,25 @@ func newExporter(cfg component.Config, set exporter.Settings) *baseExporter {
6262
// start actually creates the gRPC connection. The client construction is deferred till this point as this
6363
// is the only place we get hold of Extensions which are required to construct auth round tripper.
6464
func (e *baseExporter) start(ctx context.Context, host component.Host) (err error) {
65-
agentOpt := configgrpc.WithGrpcDialOption(grpc.WithUserAgent(e.userAgent))
65+
// If the user configured a User-Agent header, use it for the dial option.
66+
// grpc.WithUserAgent sets the value at the transport level and takes precedence
67+
// over per-call metadata, so we must pass the user's value here directly.
68+
userAgent := e.userAgent
69+
headers := map[string]string{}
70+
for k, v := range e.config.ClientConfig.Headers.Iter {
71+
headers[k] = string(v)
72+
if strings.EqualFold(k, "User-Agent") {
73+
userAgent = string(v)
74+
}
75+
}
76+
agentOpt := configgrpc.WithGrpcDialOption(grpc.WithUserAgent(userAgent))
6677
if e.clientConn, err = e.config.ClientConfig.ToClientConn(ctx, host.GetExtensions(), e.settings, agentOpt); err != nil {
6778
return err
6879
}
6980
e.traceExporter = ptraceotlp.NewGRPCClient(e.clientConn)
7081
e.metricExporter = pmetricotlp.NewGRPCClient(e.clientConn)
7182
e.logExporter = plogotlp.NewGRPCClient(e.clientConn)
7283
e.profileExporter = pprofileotlp.NewGRPCClient(e.clientConn)
73-
headers := map[string]string{}
74-
for k, v := range e.config.ClientConfig.Headers.Iter {
75-
headers[k] = string(v)
76-
}
7784
e.metadata = metadata.New(headers)
7885
e.callOptions = []grpc.CallOption{
7986
grpc.WaitForReady(e.config.ClientConfig.WaitForReady),

exporter/otlpexporter/otlp_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,3 +1042,50 @@ 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+
Headers: configopaque.MapList{
1063+
{Name: "User-Agent", Value: "My Distribution For The Collector"},
1064+
},
1065+
}
1066+
set := exportertest.NewNopSettings(factory.Type())
1067+
set.BuildInfo.Description = "Collector"
1068+
set.BuildInfo.Version = "1.2.3test"
1069+
1070+
exp, err := factory.CreateMetrics(context.Background(), set, cfg)
1071+
require.NoError(t, err)
1072+
require.NotNil(t, exp)
1073+
defer func() {
1074+
assert.NoError(t, exp.Shutdown(context.Background()))
1075+
}()
1076+
1077+
require.NoError(t, exp.Start(context.Background(), componenttest.NewNopHost()))
1078+
1079+
// Send a metric to trigger an RPC.
1080+
require.NoError(t, exp.ConsumeMetrics(context.Background(), pmetric.NewMetrics()))
1081+
1082+
assert.Eventually(t, func() bool {
1083+
return rcv.requestCount.Load() > 0
1084+
}, 10*time.Second, 5*time.Millisecond)
1085+
1086+
md := rcv.getMetadata()
1087+
require.Len(t, md.Get("User-Agent"), 1)
1088+
// User-configured value must win over the builtin BuildInfo agent.
1089+
require.Contains(t, md.Get("User-Agent")[0], "My Distribution For The Collector")
1090+
require.NotContains(t, md.Get("User-Agent")[0], "Collector/1.2.3test")
1091+
}

0 commit comments

Comments
 (0)