@@ -20,17 +20,26 @@ import (
2020 "google.golang.org/grpc"
2121 "google.golang.org/grpc/backoff"
2222 "google.golang.org/grpc/codes"
23+ "google.golang.org/grpc/credentials/insecure"
2324 "google.golang.org/grpc/encoding/gzip"
2425 "google.golang.org/grpc/metadata"
2526 "google.golang.org/grpc/status"
2627
28+ "go.opentelemetry.io/otel"
2729 "go.opentelemetry.io/otel/attribute"
2830 "go.opentelemetry.io/otel/exporters/otlp/otlptrace"
2931 "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
3032 "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal"
33+ "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/counter"
34+ "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/observ"
3135 "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/otlptracetest"
36+ "go.opentelemetry.io/otel/sdk/instrumentation"
37+ "go.opentelemetry.io/otel/sdk/metric"
38+ "go.opentelemetry.io/otel/sdk/metric/metricdata"
39+ "go.opentelemetry.io/otel/sdk/metric/metricdata/metricdatatest"
3240 sdktrace "go.opentelemetry.io/otel/sdk/trace"
3341 "go.opentelemetry.io/otel/sdk/trace/tracetest"
42+ "go.opentelemetry.io/otel/semconv/v1.37.0/otelconv"
3443)
3544
3645func TestMain (m * testing.M ) {
@@ -115,7 +124,7 @@ func TestWithEndpointURL(t *testing.T) {
115124}
116125
117126func newGRPCExporter (
118- t * testing.T ,
127+ tb testing.TB ,
119128 ctx context.Context ,
120129 endpoint string ,
121130 additionalOpts ... otlptracegrpc.Option ,
@@ -130,7 +139,7 @@ func newGRPCExporter(
130139 client := otlptracegrpc .NewClient (opts ... )
131140 exp , err := otlptrace .New (ctx , client )
132141 if err != nil {
133- t .Fatalf ("failed to create a new collector exporter: %v" , err )
142+ tb .Fatalf ("failed to create a new collector exporter: %v" , err )
134143 }
135144 return exp
136145}
@@ -430,3 +439,161 @@ func TestCustomUserAgent(t *testing.T) {
430439 headers := mc .getHeaders ()
431440 require .Contains (t , headers .Get ("user-agent" )[0 ], customUserAgent )
432441}
442+
443+ func TestClientInstrumentation (t * testing.T ) {
444+ // Enable instrumentation for this test.
445+ t .Setenv ("OTEL_GO_X_OBSERVABILITY" , "true" )
446+
447+ // Reset client ID to be deterministic
448+ const id = 0
449+ counter .SetExporterID (id )
450+
451+ // Save original meter provider and restore at end of test.
452+ orig := otel .GetMeterProvider ()
453+ t .Cleanup (func () { otel .SetMeterProvider (orig ) })
454+
455+ // Create a new meter provider to capture metrics.
456+ reader := metric .NewManualReader ()
457+ mp := metric .NewMeterProvider (metric .WithReader (reader ))
458+ otel .SetMeterProvider (mp )
459+
460+ const n , msg = 2 , "partially successful"
461+ mc := runMockCollectorWithConfig (t , & mockConfig {
462+ endpoint : "localhost:0" , // Determine canonical endpoint.
463+ partial : & coltracepb.ExportTracePartialSuccess {
464+ RejectedSpans : n ,
465+ ErrorMessage : msg ,
466+ },
467+ })
468+ t .Cleanup (func () { require .NoError (t , mc .stop ()) })
469+
470+ exp := newGRPCExporter (t , t .Context (), mc .endpoint )
471+ err := exp .ExportSpans (t .Context (), roSpans )
472+ assert .ErrorIs (t , err , internal .TracePartialSuccessError (n , msg ))
473+ require .NoError (t , exp .Shutdown (t .Context ()))
474+
475+ var got metricdata.ResourceMetrics
476+ require .NoError (t , reader .Collect (t .Context (), & got ))
477+
478+ attrs := observ .BaseAttrs (id , canonical (t , mc .endpoint ))
479+ want := metricdata.ScopeMetrics {
480+ Scope : instrumentation.Scope {
481+ Name : observ .ScopeName ,
482+ Version : observ .Version ,
483+ SchemaURL : observ .SchemaURL ,
484+ },
485+ Metrics : []metricdata.Metrics {
486+ {
487+ Name : otelconv.SDKExporterSpanInflight {}.Name (),
488+ Description : otelconv.SDKExporterSpanInflight {}.Description (),
489+ Unit : otelconv.SDKExporterSpanInflight {}.Unit (),
490+ Data : metricdata.Sum [int64 ]{
491+ DataPoints : []metricdata.DataPoint [int64 ]{
492+ {Attributes : attribute .NewSet (attrs ... )},
493+ },
494+ Temporality : metricdata .CumulativeTemporality ,
495+ },
496+ },
497+ {
498+ Name : otelconv.SDKExporterSpanExported {}.Name (),
499+ Description : otelconv.SDKExporterSpanExported {}.Description (),
500+ Unit : otelconv.SDKExporterSpanExported {}.Unit (),
501+ Data : metricdata.Sum [int64 ]{
502+ DataPoints : []metricdata.DataPoint [int64 ]{
503+ {Attributes : attribute .NewSet (attrs ... )},
504+ {Attributes : attribute .NewSet (append (
505+ attrs ,
506+ otelconv.SDKExporterSpanExported {}.AttrErrorType ("*errors.joinError" ),
507+ )... )},
508+ },
509+ Temporality : 0x1 ,
510+ IsMonotonic : true ,
511+ },
512+ },
513+ {
514+ Name : otelconv.SDKExporterOperationDuration {}.Name (),
515+ Description : otelconv.SDKExporterOperationDuration {}.Description (),
516+ Unit : otelconv.SDKExporterOperationDuration {}.Unit (),
517+ Data : metricdata.Histogram [float64 ]{
518+ DataPoints : []metricdata.HistogramDataPoint [float64 ]{
519+ {Attributes : attribute .NewSet (append (
520+ attrs ,
521+ otelconv.SDKExporterOperationDuration {}.AttrErrorType ("*errors.joinError" ),
522+ otelconv.SDKExporterOperationDuration {}.AttrRPCGRPCStatusCode (
523+ otelconv .RPCGRPCStatusCodeOk ,
524+ ),
525+ )... )},
526+ },
527+ Temporality : 0x1 ,
528+ },
529+ },
530+ },
531+ }
532+ require .Len (t , got .ScopeMetrics , 1 )
533+ opt := []metricdatatest.Option {
534+ metricdatatest .IgnoreTimestamp (),
535+ metricdatatest .IgnoreExemplars (),
536+ metricdatatest .IgnoreValue (),
537+ }
538+ metricdatatest .AssertEqual (t , want , got .ScopeMetrics [0 ], opt ... )
539+ }
540+
541+ func canonical (t * testing.T , endpoint string ) string {
542+ t .Helper ()
543+
544+ opt := grpc .WithTransportCredentials (insecure .NewCredentials ())
545+ c , err := grpc .NewClient (endpoint , opt ) // Used to normaliz endpoint.
546+ if err != nil {
547+ t .Fatalf ("failed to create grpc client: %v" , err )
548+ }
549+ out := c .CanonicalTarget ()
550+ _ = c .Close ()
551+
552+ return out
553+ }
554+
555+ func BenchmarkExporterExportSpans (b * testing.B ) {
556+ const n = 10
557+
558+ run := func (b * testing.B ) {
559+ mc := runMockCollectorWithConfig (b , & mockConfig {
560+ endpoint : "localhost:0" ,
561+ partial : & coltracepb.ExportTracePartialSuccess {
562+ RejectedSpans : 5 ,
563+ ErrorMessage : "partially successful" ,
564+ },
565+ })
566+ b .Cleanup (func () { require .NoError (b , mc .stop ()) })
567+
568+ exp := newGRPCExporter (b , b .Context (), mc .endpoint )
569+ b .Cleanup (func () {
570+ //nolint:usetesting // required to avoid getting a canceled context at cleanup.
571+ assert .NoError (b , exp .Shutdown (context .Background ()))
572+ })
573+
574+ stubs := make ([]tracetest.SpanStub , n )
575+ for i := range stubs {
576+ stubs [i ].Name = fmt .Sprintf ("Span %d" , i )
577+ }
578+ spans := tracetest .SpanStubs (stubs ).Snapshots ()
579+
580+ b .ReportAllocs ()
581+ b .ResetTimer ()
582+
583+ var err error
584+ for b .Loop () {
585+ err = exp .ExportSpans (b .Context (), spans )
586+ }
587+ _ = err
588+ }
589+
590+ b .Run ("Observability" , func (b * testing.B ) {
591+ b .Setenv ("OTEL_GO_X_OBSERVABILITY" , "true" )
592+ run (b )
593+ })
594+
595+ b .Run ("NoObservability" , func (b * testing.B ) {
596+ b .Setenv ("OTEL_GO_X_OBSERVABILITY" , "false" )
597+ run (b )
598+ })
599+ }
0 commit comments