diff --git a/internal/settings.go b/internal/settings.go index a81d149ae22..3b1f2bd10b6 100644 --- a/internal/settings.go +++ b/internal/settings.go @@ -15,6 +15,8 @@ import ( "time" "cloud.google.com/go/auth" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "golang.org/x/oauth2" "golang.org/x/oauth2/google" "google.golang.org/api/internal/impersonate" @@ -76,6 +78,11 @@ type DialSettings struct { // TODO(b/372244283): Remove after b/358175516 has been fixed EnableAsyncRefreshDryRun func() + + // otelhttp/otelgrpc options + OpenTelemetryOpts []any + OpenTelemetryOptsGRPC []otelgrpc.Option + OpenTelemetryOptsHTTP []otelhttp.Option } // GetScopes returns the user-provided scopes, if set, or else falls back to the @@ -181,6 +188,18 @@ func (ds *DialSettings) Validate() error { if ds.ImpersonationConfig != nil && len(ds.ImpersonationConfig.Scopes) == 0 && len(ds.Scopes) == 0 { return errors.New("WithImpersonatedCredentials requires scopes being provided") } + for _, opt := range ds.OpenTelemetryOpts { + switch o := opt.(type) { + case otelhttp.Option: + ds.OpenTelemetryOptsHTTP = append(ds.OpenTelemetryOptsHTTP, o) + case otelgrpc.Option: + ds.OpenTelemetryOptsGRPC = append(ds.OpenTelemetryOptsGRPC, o) + default: + return errors.New("WithOpenTelemetryOpts options must be of type " + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp.Option " + + "or go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc.Option") + } + } return nil } diff --git a/internal/settings_test.go b/internal/settings_test.go index 09ccd2d4985..f1f39681304 100644 --- a/internal/settings_test.go +++ b/internal/settings_test.go @@ -12,7 +12,10 @@ import ( "google.golang.org/api/internal/impersonate" "google.golang.org/grpc" + "google.golang.org/grpc/stats" + "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" "golang.org/x/oauth2" "golang.org/x/oauth2/google" ) @@ -38,6 +41,13 @@ func TestSettingsValidate(t *testing.T) { {ClientCertSource: dummyGetClientCertificate}, {ImpersonationConfig: &impersonate.Config{Scopes: []string{"x"}}}, {ImpersonationConfig: &impersonate.Config{}, Scopes: []string{"x"}}, + {OpenTelemetryOpts: []any{ + otelgrpc.WithFilter(func(ri *stats.RPCTagInfo) bool { + return true + }), + otelhttp.WithFilter(func(ri *http.Request) bool { + return true + })}}, } { err := ds.Validate() if err != nil { @@ -67,6 +77,7 @@ func TestSettingsValidate(t *testing.T) { {ClientCertSource: dummyGetClientCertificate, GRPCDialOpts: []grpc.DialOption{grpc.WithInsecure()}}, {ClientCertSource: dummyGetClientCertificate, GRPCConnPoolSize: 1}, {ImpersonationConfig: &impersonate.Config{}}, + {OpenTelemetryOpts: []any{"string"}}, } { err := ds.Validate() if err == nil { diff --git a/option/option.go b/option/option.go index 1b134caa862..4ca24c366d8 100644 --- a/option/option.go +++ b/option/option.go @@ -414,3 +414,20 @@ type withLogger struct{ l *slog.Logger } func (w withLogger) Apply(o *internal.DialSettings) { o.Logger = w.l } + +// WithOpenTelemetryOpts returns a ClientOption that sets the options for +// the OpenTelemetry HTTP/gRPC client. This option is used to configure +// the OpenTelemetry HTTP/gRPC client instrumentation. +// It can accept any number of options, which can be +// go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp.Option or +// go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc.Option +func WithOpenTelemetryOpts(opts ...any) ClientOption { + return withOpenTelemetryOpts{opts} +} + +type withOpenTelemetryOpts struct{ opts []any } + +func (w withOpenTelemetryOpts) Apply(o *internal.DialSettings) { + o.OpenTelemetryOpts = make([]any, len(w.opts)) + copy(o.OpenTelemetryOpts, w.opts) +} diff --git a/option/option_test.go b/option/option_test.go index 04c3716513b..b4b6de90088 100644 --- a/option/option_test.go +++ b/option/option_test.go @@ -133,3 +133,23 @@ func TestApplyClientCertSource(t *testing.T) { t.Error(cmp.Diff(certGot, certWant, cmpopts.IgnoreUnexported(big.Int{}), cmpopts.IgnoreFields(tls.Certificate{}, "Leaf"))) } } + +func TestOpenTelemetryOpts(t *testing.T) { + otelOpts := []any{ + "non-otelhttp-otelgrpc-option", + } + opts := []ClientOption{ + WithOpenTelemetryOpts(otelOpts...), + } + var got internal.DialSettings + for _, opt := range opts { + opt.Apply(&got) + } + want := internal.DialSettings{ + OpenTelemetryOpts: []any{}, + } + + if cmp.Equal(got, want) { + t.Error(cmp.Diff(got, want)) + } +} diff --git a/transport/grpc/dial.go b/transport/grpc/dial.go index a6630a0e440..35f7abd15f7 100644 --- a/transport/grpc/dial.go +++ b/transport/grpc/dial.go @@ -68,9 +68,9 @@ var ( // otelGRPCStatsHandler returns singleton otelStatsHandler for reuse across all // dial connections. -func otelGRPCStatsHandler() stats.Handler { +func otelGRPCStatsHandler(opts []otelgrpc.Option) stats.Handler { initOtelStatsHandlerOnce.Do(func() { - otelStatsHandler = otelgrpc.NewClientHandler() + otelStatsHandler = otelgrpc.NewClientHandler(opts...) }) return otelStatsHandler } @@ -400,7 +400,8 @@ func addOpenTelemetryStatsHandler(opts []grpc.DialOption, settings *internal.Dia if settings.TelemetryDisabled { return opts } - return append(opts, grpc.WithStatsHandler(otelGRPCStatsHandler())) + otelOpts := settings.OpenTelemetryOptsGRPC + return append(opts, grpc.WithStatsHandler(otelGRPCStatsHandler(otelOpts))) } // grpcTokenSource supplies PerRPCCredentials from an oauth.TokenSource. diff --git a/transport/http/dial.go b/transport/http/dial.go index a33df912035..3de2c16cc91 100644 --- a/transport/http/dial.go +++ b/transport/http/dial.go @@ -108,6 +108,8 @@ func newClientNewAuth(ctx context.Context, base http.RoundTripper, ds *internal. if ds.UserAgent != "" { headers.Set("User-Agent", ds.UserAgent) } + // TODO: propagate settings.OpenTelemetryOptsHTTP + // see https://github.com/googleapis/google-api-go-client/pull/3130#discussion_r2091318522 client, err := httptransport.NewClient(&httptransport.Options{ DisableTelemetry: ds.TelemetryDisabled, DisableAuthentication: ds.NoAuth, @@ -306,7 +308,7 @@ func addOpenTelemetryTransport(trans http.RoundTripper, settings *internal.DialS if settings.TelemetryDisabled { return trans } - return otelhttp.NewTransport(trans) + return otelhttp.NewTransport(trans, settings.OpenTelemetryOptsHTTP...) } // clonedTransport returns the given RoundTripper as a cloned *http.Transport.