Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .chloggen/fix-otlp-grpc-user-agent.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
change_type: enhancement
component: pkg/config/configgrpc
note: Add `UserAgent` field to `ClientConfig` to allow overriding the default gRPC user-agent string.
issues: [14686]
subtext: |
The otlp gRPC exporter was unconditionally setting the User-Agent via
grpc.WithUserAgent() at dial time, which takes precedence over per-call
metadata, causing any user-configured User-Agent to be silently discarded.
A dedicated `UserAgent` field has been added to `ClientConfig` which, when
set, is used in the dial option directly instead of the default BuildInfo-derived string.
9 changes: 9 additions & 0 deletions config/configgrpc/configgrpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,11 @@ type ClientConfig struct {
// The headers associated with gRPC requests.
Headers configopaque.MapList `mapstructure:"headers,omitempty"`

// UserAgent overrides the default user-agent header sent on gRPC requests.
// The default is derived from the build info. When empty, the caller controls
// the user-agent via grpc.WithUserAgent or similar options.
UserAgent string `mapstructure:"user_agent,omitempty"`
Comment thread
57Ajay marked this conversation as resolved.

// Sets the balancer in grpclb_policy to discover the servers. Default is pick_first.
// https://github.com/grpc/grpc-go/blob/master/examples/features/load_balancing/README.md
BalancerName string `mapstructure:"balancer_name"`
Expand Down Expand Up @@ -441,6 +446,10 @@ func (cc *ClientConfig) getGrpcDialOptions(
}
}

if cc.UserAgent != "" {
opts = append(opts, grpc.WithUserAgent(cc.UserAgent))
}

return opts, nil
}

Expand Down
22 changes: 22 additions & 0 deletions config/configgrpc/configgrpc_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1319,3 +1319,25 @@ func tempSocketName(t *testing.T) string {
require.NoError(t, os.Remove(socket))
return socket
}

func TestGrpcClientUserAgent(t *testing.T) {
cc := &ClientConfig{
TLS: configtls.ClientConfig{
Insecure: true,
},
UserAgent: "my-distribution/1.0",
}
opts, err := cc.getGrpcDialOptions(
context.Background(),
nil,
componenttest.NewNopTelemetrySettings(),
[]ToClientConnOption{},
)
require.NoError(t, err)
/* Expecting 3 DialOptions:
* - WithTransportCredentials (TLS)
* - WithStatsHandler (always, for self-telemetry)
* - WithUserAgent (from UserAgent field)
*/
assert.Len(t, opts, 3)
}
46 changes: 46 additions & 0 deletions exporter/otlpexporter/otlp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net"
"path/filepath"
"runtime"
"strings"
"sync"
"sync/atomic"
"testing"
Expand Down Expand Up @@ -1042,3 +1043,48 @@ func TestSendProfilesWhenEndpointHasHttpScheme(t *testing.T) {
})
}
}

func TestUserAgentHeader_OverriddenByConfig(t *testing.T) {
// Start an OTLP-compatible receiver.
ln, err := net.Listen("tcp", "localhost:")
require.NoError(t, err, "Failed to find an available address to run the gRPC server: %v", err)
rcv := otlpMetricsReceiverOnGRPCServer(ln)
defer rcv.srv.GracefulStop()

// Start an OTLP exporter with a custom User-Agent header.
factory := NewFactory()
cfg := factory.CreateDefaultConfig().(*Config)
cfg.QueueConfig = configoptional.None[exporterhelper.QueueBatchConfig]()
cfg.ClientConfig = configgrpc.ClientConfig{
Endpoint: ln.Addr().String(),
TLS: configtls.ClientConfig{
Insecure: true,
},
UserAgent: "My Distribution For The Collector",
}
set := exportertest.NewNopSettings(factory.Type())
set.BuildInfo.Description = "Collector"
set.BuildInfo.Version = "1.2.3test"

exp, err := factory.CreateMetrics(context.Background(), set, cfg)
require.NoError(t, err)
require.NotNil(t, exp)
defer func() {
assert.NoError(t, exp.Shutdown(context.Background()))
}()

require.NoError(t, exp.Start(context.Background(), componenttest.NewNopHost()))

// Send a metric to trigger an RPC.
require.NoError(t, exp.ConsumeMetrics(context.Background(), pmetric.NewMetrics()))

assert.Eventually(t, func() bool {
return rcv.requestCount.Load() > 0
}, 10*time.Second, 5*time.Millisecond)

md := rcv.getMetadata()
require.Len(t, md.Get("User-Agent"), 1)
// User-configured value must win over the builtin BuildInfo agent.
require.True(t, strings.HasPrefix(md.Get("User-Agent")[0], "My Distribution For The Collector"))
require.NotContains(t, md.Get("User-Agent")[0], "Collector/1.2.3test")
Comment thread
57Ajay marked this conversation as resolved.
}
Loading