Skip to content

Commit

Permalink
feat: informative user-agent for otlp exporters (#3970)
Browse files Browse the repository at this point in the history
* feat: collector-aware user-agent header

- add collector build info to otlp/otlphttp exporter headers

* Address PR comments

- move user-agent additions to the exporters
- add OS and ARCH to user-agent

* encase user-agent os/arch in parenthesis

* add doc comment

* move otlp-http user-agent to exporter

* formatting

* revert unnecessary diff

* formatting

* http exporter: use a custom round tripper, clone request

* RoundTrip must always close the body

* remove custom roundtripper

* fix lint issue

* update changelog

Co-authored-by: Alex Boten <[email protected]>
  • Loading branch information
vreynolds and Alex Boten authored Dec 3, 2021
1 parent 9309a1f commit acf6556
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@

- Add semconv 1.7.0 and 1.8.0 (#4452)
- Added `feature-gates` CLI flag for controlling feature gate state. (#4368)
- Add a default user-agent header to the OTLP/gRPC and OTLP/HTTP exporters containing collector build information (#3970)

## v0.39.0 Beta

Expand Down
6 changes: 3 additions & 3 deletions exporter/otlpexporter/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ func createTracesExporter(
set component.ExporterCreateSettings,
cfg config.Exporter,
) (component.TracesExporter, error) {
oce, err := newExporter(cfg, set.TelemetrySettings)
oce, err := newExporter(cfg, set.TelemetrySettings, set.BuildInfo)
if err != nil {
return nil, err
}
Expand All @@ -80,7 +80,7 @@ func createMetricsExporter(
set component.ExporterCreateSettings,
cfg config.Exporter,
) (component.MetricsExporter, error) {
oce, err := newExporter(cfg, set.TelemetrySettings)
oce, err := newExporter(cfg, set.TelemetrySettings, set.BuildInfo)
if err != nil {
return nil, err
}
Expand All @@ -103,7 +103,7 @@ func createLogsExporter(
set component.ExporterCreateSettings,
cfg config.Exporter,
) (component.LogsExporter, error) {
oce, err := newExporter(cfg, set.TelemetrySettings)
oce, err := newExporter(cfg, set.TelemetrySettings, set.BuildInfo)
if err != nil {
return nil, err
}
Expand Down
13 changes: 11 additions & 2 deletions exporter/otlpexporter/otlp.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package otlpexporter // import "go.opentelemetry.io/collector/exporter/otlpexpor
import (
"context"
"errors"
"fmt"
"runtime"
"time"

"google.golang.org/genproto/googleapis/rpc/errdetails"
Expand Down Expand Up @@ -46,18 +48,24 @@ type exporter struct {
callOptions []grpc.CallOption

settings component.TelemetrySettings

// Default user-agent header.
userAgent string
}

// Crete new exporter and start it. The exporter will begin connecting but
// this function may return before the connection is established.
func newExporter(cfg config.Exporter, settings component.TelemetrySettings) (*exporter, error) {
func newExporter(cfg config.Exporter, settings component.TelemetrySettings, buildInfo component.BuildInfo) (*exporter, error) {
oCfg := cfg.(*Config)

if oCfg.Endpoint == "" {
return nil, errors.New("OTLP exporter config requires an Endpoint")
}

return &exporter{config: oCfg, settings: settings}, nil
userAgent := fmt.Sprintf("%s/%s (%s/%s)",
buildInfo.Description, buildInfo.Version, runtime.GOOS, runtime.GOARCH)

return &exporter{config: oCfg, settings: settings, userAgent: userAgent}, nil
}

// start actually creates the gRPC connection. The client construction is deferred till this point as this
Expand All @@ -67,6 +75,7 @@ func (e *exporter) start(_ context.Context, host component.Host) (err error) {
if err != nil {
return err
}
dialOpts = append(dialOpts, grpc.WithUserAgent(e.userAgent))

if e.clientConn, err = grpc.Dial(e.config.GRPCClientSettings.SanitizedEndpoint(), dialOpts...); err != nil {
return err
Expand Down
20 changes: 18 additions & 2 deletions exporter/otlpexporter/otlp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,8 @@ func TestSendTraces(t *testing.T) {
},
}
set := componenttest.NewNopExporterCreateSettings()
set.BuildInfo.Description = "Collector"
set.BuildInfo.Version = "1.2.3test"
exp, err := factory.CreateTracesExporter(context.Background(), set, cfg)
require.NoError(t, err)
require.NotNil(t, exp)
Expand Down Expand Up @@ -248,7 +250,10 @@ func TestSendTraces(t *testing.T) {
assert.EqualValues(t, 2, atomic.LoadInt32(&rcv.requestCount))
assert.EqualValues(t, td, rcv.GetLastRequest())

require.EqualValues(t, rcv.GetMetadata().Get("header"), expectedHeader)
md := rcv.GetMetadata()
require.EqualValues(t, md.Get("header"), expectedHeader)
require.Equal(t, len(md.Get("User-Agent")), 1)
require.Contains(t, md.Get("User-Agent")[0], "Collector/1.2.3test")
}

func TestSendTracesWhenEndpointHasHttpScheme(t *testing.T) {
Expand Down Expand Up @@ -345,6 +350,8 @@ func TestSendMetrics(t *testing.T) {
},
}
set := componenttest.NewNopExporterCreateSettings()
set.BuildInfo.Description = "Collector"
set.BuildInfo.Version = "1.2.3test"
exp, err := factory.CreateMetricsExporter(context.Background(), set, cfg)
require.NoError(t, err)
require.NotNil(t, exp)
Expand Down Expand Up @@ -389,7 +396,10 @@ func TestSendMetrics(t *testing.T) {
assert.EqualValues(t, 4, atomic.LoadInt32(&rcv.totalItems))
assert.EqualValues(t, md, rcv.GetLastRequest())

require.EqualValues(t, rcv.GetMetadata().Get("header"), expectedHeader)
mdata := rcv.GetMetadata()
require.EqualValues(t, mdata.Get("header"), expectedHeader)
require.Equal(t, len(mdata.Get("User-Agent")), 1)
require.Contains(t, mdata.Get("User-Agent")[0], "Collector/1.2.3test")
}

func TestSendTraceDataServerDownAndUp(t *testing.T) {
Expand Down Expand Up @@ -546,6 +556,8 @@ func TestSendLogData(t *testing.T) {
},
}
set := componenttest.NewNopExporterCreateSettings()
set.BuildInfo.Description = "Collector"
set.BuildInfo.Version = "1.2.3test"
exp, err := factory.CreateLogsExporter(context.Background(), set, cfg)
require.NoError(t, err)
require.NotNil(t, exp)
Expand Down Expand Up @@ -587,4 +599,8 @@ func TestSendLogData(t *testing.T) {
assert.EqualValues(t, 2, atomic.LoadInt32(&rcv.requestCount))
assert.EqualValues(t, 2, atomic.LoadInt32(&rcv.totalItems))
assert.EqualValues(t, ld, rcv.GetLastRequest())

md := rcv.GetMetadata()
require.Equal(t, len(md.Get("User-Agent")), 1)
require.Contains(t, md.Get("User-Agent")[0], "Collector/1.2.3test")
}
6 changes: 3 additions & 3 deletions exporter/otlphttpexporter/factory.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func createTracesExporter(
set component.ExporterCreateSettings,
cfg config.Exporter,
) (component.TracesExporter, error) {
oce, err := newExporter(cfg, set.Logger)
oce, err := newExporter(cfg, set.Logger, set.BuildInfo)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -105,7 +105,7 @@ func createMetricsExporter(
set component.ExporterCreateSettings,
cfg config.Exporter,
) (component.MetricsExporter, error) {
oce, err := newExporter(cfg, set.Logger)
oce, err := newExporter(cfg, set.Logger, set.BuildInfo)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -133,7 +133,7 @@ func createLogsExporter(
set component.ExporterCreateSettings,
cfg config.Exporter,
) (component.LogsExporter, error) {
oce, err := newExporter(cfg, set.Logger)
oce, err := newExporter(cfg, set.Logger, set.BuildInfo)
if err != nil {
return nil, err
}
Expand Down
15 changes: 12 additions & 3 deletions exporter/otlphttpexporter/otlp.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"io/ioutil"
"net/http"
"net/url"
"runtime"
"strconv"
"strings"
"time"
Expand All @@ -49,6 +50,9 @@ type exporter struct {
metricsURL string
logsURL string
logger *zap.Logger

// Default user-agent header.
userAgent string
}

const (
Expand All @@ -57,7 +61,7 @@ const (
)

// Crete new exporter.
func newExporter(cfg config.Exporter, logger *zap.Logger) (*exporter, error) {
func newExporter(cfg config.Exporter, logger *zap.Logger, buildInfo component.BuildInfo) (*exporter, error) {
oCfg := cfg.(*Config)

if oCfg.Endpoint != "" {
Expand All @@ -67,10 +71,14 @@ func newExporter(cfg config.Exporter, logger *zap.Logger) (*exporter, error) {
}
}

userAgent := fmt.Sprintf("%s/%s (%s/%s)",
buildInfo.Description, buildInfo.Version, runtime.GOOS, runtime.GOARCH)

// client construction is deferred to start
return &exporter{
config: oCfg,
logger: logger,
config: oCfg,
logger: logger,
userAgent: userAgent,
}, nil
}

Expand Down Expand Up @@ -132,6 +140,7 @@ func (e *exporter) export(ctx context.Context, url string, request []byte) error
return consumererror.NewPermanent(err)
}
req.Header.Set("Content-Type", "application/x-protobuf")
req.Header.Set("User-Agent", e.userAgent)

resp, err := e.client.Do(req)
if err != nil {
Expand Down
164 changes: 164 additions & 0 deletions exporter/otlphttpexporter/otlp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -529,3 +529,167 @@ func TestErrorResponses(t *testing.T) {
})
}
}

func TestUserAgent(t *testing.T) {
addr := testutil.GetAvailableLocalAddress(t)
set := componenttest.NewNopExporterCreateSettings()
set.BuildInfo.Description = "Collector"
set.BuildInfo.Version = "1.2.3test"

tests := []struct {
name string
headers map[string]string
expectedUA string
}{
{
name: "default_user_agent",
expectedUA: "Collector/1.2.3test",
},
{
name: "custom_user_agent",
headers: map[string]string{"User-Agent": "My Custom Agent"},
expectedUA: "My Custom Agent",
},
{
name: "custom_user_agent_lowercase",
headers: map[string]string{"user-agent": "My Custom Agent"},
expectedUA: "My Custom Agent",
},
}

t.Run("traces", func(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/v1/traces", func(writer http.ResponseWriter, request *http.Request) {
assert.Contains(t, request.Header.Get("user-agent"), test.expectedUA)
writer.WriteHeader(200)
})
srv := http.Server{
Addr: addr,
Handler: mux,
}
ln, err := net.Listen("tcp", addr)
require.NoError(t, err)
go func() {
_ = srv.Serve(ln)
}()

cfg := &Config{
ExporterSettings: config.NewExporterSettings(config.NewComponentID(typeStr)),
TracesEndpoint: fmt.Sprintf("http://%s/v1/traces", addr),
HTTPClientSettings: confighttp.HTTPClientSettings{
Headers: test.headers,
},
}
exp, err := createTracesExporter(context.Background(), set, cfg)
require.NoError(t, err)

// start the exporter
err = exp.Start(context.Background(), componenttest.NewNopHost())
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, exp.Shutdown(context.Background()))
})

// generate data
traces := pdata.NewTraces()
err = exp.ConsumeTraces(context.Background(), traces)
require.NoError(t, err)

srv.Close()
})
}
})

t.Run("metrics", func(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/v1/metrics", func(writer http.ResponseWriter, request *http.Request) {
assert.Contains(t, request.Header.Get("user-agent"), test.expectedUA)
writer.WriteHeader(200)
})
srv := http.Server{
Addr: addr,
Handler: mux,
}
ln, err := net.Listen("tcp", addr)
require.NoError(t, err)
go func() {
_ = srv.Serve(ln)
}()

cfg := &Config{
ExporterSettings: config.NewExporterSettings(config.NewComponentID(typeStr)),
MetricsEndpoint: fmt.Sprintf("http://%s/v1/metrics", addr),
HTTPClientSettings: confighttp.HTTPClientSettings{
Headers: test.headers,
},
}
exp, err := createMetricsExporter(context.Background(), set, cfg)
require.NoError(t, err)

// start the exporter
err = exp.Start(context.Background(), componenttest.NewNopHost())
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, exp.Shutdown(context.Background()))
})

// generate data
metrics := pdata.NewMetrics()
err = exp.ConsumeMetrics(context.Background(), metrics)
require.NoError(t, err)

srv.Close()
})
}
})

t.Run("logs", func(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/v1/logs", func(writer http.ResponseWriter, request *http.Request) {
assert.Contains(t, request.Header.Get("user-agent"), test.expectedUA)
writer.WriteHeader(200)
})
srv := http.Server{
Addr: addr,
Handler: mux,
}
ln, err := net.Listen("tcp", addr)
require.NoError(t, err)
go func() {
_ = srv.Serve(ln)
}()

cfg := &Config{
ExporterSettings: config.NewExporterSettings(config.NewComponentID(typeStr)),
LogsEndpoint: fmt.Sprintf("http://%s/v1/logs", addr),
HTTPClientSettings: confighttp.HTTPClientSettings{
Headers: test.headers,
},
}
exp, err := createLogsExporter(context.Background(), set, cfg)
require.NoError(t, err)

// start the exporter
err = exp.Start(context.Background(), componenttest.NewNopHost())
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, exp.Shutdown(context.Background()))
})

// generate data
logs := pdata.NewLogs()
err = exp.ConsumeLogs(context.Background(), logs)
require.NoError(t, err)

srv.Close()

})
}
})
}

0 comments on commit acf6556

Please sign in to comment.