diff --git a/CHANGELOG.md b/CHANGELOG.md index 1afd5e7b250..e1a68c5a3e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm ### Added - Added an example of using metric views to customize instruments. (#3177) +- Add default User-Agent header to OTLP exporter requests (`go.opentelemetry.io/otel/exporters/otlpmetric/otlpmetricgrpc`, `go.opentelemetry.io/otel/exporters/otlpmetric/otlpmetrichttp`, `go.opentelemetry.io/otel/exporters/otlptrace/otlptracegrpc` and `go.opentelemetry.io/otel/exporters/otlptrace/otlptracehttp`). (#3261) ### Changed diff --git a/exporters/otlp/internal/header.go b/exporters/otlp/internal/header.go new file mode 100644 index 00000000000..9aa62ed9e8e --- /dev/null +++ b/exporters/otlp/internal/header.go @@ -0,0 +1,24 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package internal contains common functionality for all OTLP exporters. +package internal // import "go.opentelemetry.io/otel/exporters/otlp/internal" + +import "go.opentelemetry.io/otel" + +// GetUserAgentHeader return an OTLP header value form "OTel OTLP Exporter Go/{{ .Version }}" +// https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/exporter.md#user-agent +func GetUserAgentHeader() string { + return "OTel OTLP Exporter Go/" + otel.Version() +} diff --git a/exporters/otlp/internal/header_test.go b/exporters/otlp/internal/header_test.go new file mode 100644 index 00000000000..ecca1a9490e --- /dev/null +++ b/exporters/otlp/internal/header_test.go @@ -0,0 +1,26 @@ +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package internal contains common functionality for all OTLP exporters. +package internal // import "go.opentelemetry.io/otel/exporters/otlp/internal" + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestGetUserAgentHeader(t *testing.T) { + require.Regexp(t, "OTel OTLP Exporter Go/1\\..*", GetUserAgentHeader()) +} diff --git a/exporters/otlp/otlpmetric/internal/oconf/options.go b/exporters/otlp/otlpmetric/internal/oconf/options.go index f5a82d6db17..f7d44071442 100644 --- a/exporters/otlp/otlpmetric/internal/oconf/options.go +++ b/exporters/otlp/otlpmetric/internal/oconf/options.go @@ -104,6 +104,7 @@ func NewGRPCConfig(opts ...GRPCOption) Config { Timeout: DefaultTimeout, }, RetryConfig: retry.DefaultConfig, + DialOptions: []grpc.DialOption{grpc.WithUserAgent(internal.GetUserAgentHeader())}, } cfg = ApplyGRPCEnvConfigs(cfg) for _, opt := range opts { diff --git a/exporters/otlp/otlpmetric/otlpmetricgrpc/client_test.go b/exporters/otlp/otlpmetric/otlpmetricgrpc/client_test.go index 1cb8cd815e3..64d3b216a2d 100644 --- a/exporters/otlp/otlpmetric/otlpmetricgrpc/client_test.go +++ b/exporters/otlp/otlpmetric/otlpmetricgrpc/client_test.go @@ -22,6 +22,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/genproto/googleapis/rpc/errdetails" + "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/types/known/durationpb" @@ -169,6 +170,7 @@ func TestConfig(t *testing.T) { require.NoError(t, exp.Shutdown(ctx)) got := coll.Headers() + require.Regexp(t, "OTel OTLP Exporter Go/1\\..*", got) require.Contains(t, got, key) assert.Equal(t, got[key], []string{headers[key]}) }) @@ -188,4 +190,18 @@ func TestConfig(t *testing.T) { err := exp.Export(ctx, metricdata.ResourceMetrics{}) assert.ErrorContains(t, err, context.DeadlineExceeded.Error()) }) + + t.Run("WithCustomUserAgent", func(t *testing.T) { + key := "user-agent" + customerUserAgent := "custom-user-agent" + exp, coll := factoryFunc(nil, WithDialOption(grpc.WithUserAgent(customerUserAgent))) + t.Cleanup(coll.Shutdown) + ctx := context.Background() + require.NoError(t, exp.Export(ctx, metricdata.ResourceMetrics{})) + // Ensure everything is flushed. + require.NoError(t, exp.Shutdown(ctx)) + + got := coll.Headers() + assert.Contains(t, got[key][0], customerUserAgent) + }) } diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/client.go b/exporters/otlp/otlpmetric/otlpmetrichttp/client.go index 7a8d7e14707..0840a2c8fa6 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/client.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/client.go @@ -29,6 +29,7 @@ import ( "google.golang.org/protobuf/proto" + "go.opentelemetry.io/otel/exporters/otlp/internal" "go.opentelemetry.io/otel/exporters/otlp/internal/retry" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/internal/oconf" @@ -101,6 +102,8 @@ func newClient(opts ...Option) (otlpmetric.Client, error) { return nil, err } + req.Header.Set("User-Agent", internal.GetUserAgentHeader()) + if n := len(cfg.Metrics.Headers); n > 0 { for k, v := range cfg.Metrics.Headers { req.Header.Set(k, v) diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/client_test.go b/exporters/otlp/otlpmetric/otlpmetrichttp/client_test.go index 22740252b02..09a6c15d82c 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/client_test.go +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/client_test.go @@ -75,6 +75,7 @@ func TestConfig(t *testing.T) { require.NoError(t, exp.Shutdown(ctx)) got := coll.Headers() + require.Regexp(t, "OTel OTLP Exporter Go/1\\..*", got) require.Contains(t, got, key) assert.Equal(t, got[key], []string{headers[key]}) }) @@ -161,4 +162,19 @@ func TestConfig(t *testing.T) { assert.NoError(t, exp.Export(ctx, metricdata.ResourceMetrics{})) assert.Len(t, coll.Collect().Dump(), 1) }) + + t.Run("WithCustomUserAgent", func(t *testing.T) { + key := http.CanonicalHeaderKey("user-agent") + headers := map[string]string{key: "custom-user-agent"} + exp, coll := factoryFunc("", nil, WithHeaders(headers)) + ctx := context.Background() + t.Cleanup(func() { require.NoError(t, coll.Shutdown(ctx)) }) + require.NoError(t, exp.Export(ctx, metricdata.ResourceMetrics{})) + // Ensure everything is flushed. + require.NoError(t, exp.Shutdown(ctx)) + + got := coll.Headers() + require.Contains(t, got, key) + assert.Equal(t, got[key], []string{headers[key]}) + }) } diff --git a/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod b/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod index ada9ef43f21..f1f2f64706c 100644 --- a/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod +++ b/exporters/otlp/otlpmetric/otlpmetrichttp/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/stretchr/testify v1.7.1 + go.opentelemetry.io/otel v1.10.0 go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.10.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.32.1 go.opentelemetry.io/otel/metric v0.32.1 @@ -21,7 +22,6 @@ require ( github.com/google/go-cmp v0.5.8 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - go.opentelemetry.io/otel v1.10.0 // indirect go.opentelemetry.io/otel/sdk v1.10.0 // indirect go.opentelemetry.io/otel/trace v1.10.0 // indirect golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4 // indirect diff --git a/exporters/otlp/otlptrace/internal/otlpconfig/options.go b/exporters/otlp/otlptrace/internal/otlpconfig/options.go index 56e83b85334..c48ffd53081 100644 --- a/exporters/otlp/otlptrace/internal/otlpconfig/options.go +++ b/exporters/otlp/otlptrace/internal/otlpconfig/options.go @@ -97,6 +97,7 @@ func NewGRPCConfig(opts ...GRPCOption) Config { Timeout: DefaultTimeout, }, RetryConfig: retry.DefaultConfig, + DialOptions: []grpc.DialOption{grpc.WithUserAgent(internal.GetUserAgentHeader())}, } cfg = ApplyGRPCEnvConfigs(cfg) for _, opt := range opts { diff --git a/exporters/otlp/otlptrace/otlptracegrpc/client_test.go b/exporters/otlp/otlptrace/otlptracegrpc/client_test.go index d11111ed126..395ccc28bf4 100644 --- a/exporters/otlp/otlptrace/otlptracegrpc/client_test.go +++ b/exporters/otlp/otlptrace/otlptracegrpc/client_test.go @@ -212,6 +212,7 @@ func TestNewWithHeaders(t *testing.T) { require.NoError(t, exp.ExportSpans(ctx, roSpans)) headers := mc.getHeaders() + require.Regexp(t, "OTel OTLP Exporter Go/1\\..*", headers.Get("user-agent")) require.Len(t, headers.Get("header1"), 1) assert.Equal(t, "value1", headers.Get("header1")[0]) } @@ -411,3 +412,18 @@ func TestPartialSuccess(t *testing.T) { require.Contains(t, errors[0].Error(), "partially successful") require.Contains(t, errors[0].Error(), "2 spans rejected") } + +func TestCustomUserAgent(t *testing.T) { + customUserAgent := "custom-user-agent" + mc := runMockCollector(t) + t.Cleanup(func() { require.NoError(t, mc.stop()) }) + + ctx := context.Background() + exp := newGRPCExporter(t, ctx, mc.endpoint, + otlptracegrpc.WithDialOption(grpc.WithUserAgent(customUserAgent))) + t.Cleanup(func() { require.NoError(t, exp.Shutdown(ctx)) }) + require.NoError(t, exp.ExportSpans(ctx, roSpans)) + + headers := mc.getHeaders() + require.Contains(t, headers.Get("user-agent")[0], customUserAgent) +} diff --git a/exporters/otlp/otlptrace/otlptracehttp/client.go b/exporters/otlp/otlptrace/otlptracehttp/client.go index 745b6541d42..8f742dfc1bd 100644 --- a/exporters/otlp/otlptrace/otlptracehttp/client.go +++ b/exporters/otlp/otlptrace/otlptracehttp/client.go @@ -208,6 +208,8 @@ func (d *client) newRequest(body []byte) (request, error) { return request{Request: r}, err } + r.Header.Set("User-Agent", internal.GetUserAgentHeader()) + for k, v := range d.cfg.Headers { r.Header.Set(k, v) } diff --git a/exporters/otlp/otlptrace/otlptracehttp/client_test.go b/exporters/otlp/otlptrace/otlptracehttp/client_test.go index bf497bad4be..135b6517622 100644 --- a/exporters/otlp/otlptrace/otlptracehttp/client_test.go +++ b/exporters/otlp/otlptrace/otlptracehttp/client_test.go @@ -42,6 +42,10 @@ var ( "Otel-Go-Key-1": "somevalue", "Otel-Go-Key-2": "someothervalue", } + + customUserAgentHeader = map[string]string{ + "user-agent": "custome-user-agent", + } ) func TestEndToEnd(t *testing.T) { @@ -142,6 +146,15 @@ func TestEndToEnd(t *testing.T) { ExpectedHeaders: testHeaders, }, }, + { + name: "with custom user agent", + opts: []otlptracehttp.Option{ + otlptracehttp.WithHeaders(customUserAgentHeader), + }, + mcCfg: mockCollectorConfig{ + ExpectedHeaders: customUserAgentHeader, + }, + }, } for _, tc := range tests {