Skip to content

Commit c692bc4

Browse files
authored
Instrument the otlptracegrpc exporter (#7459)
Resolve #7007 ### Benchmarks ```console > benchstat inst-otlptracegrpc.bmark.result goos: linux goarch: amd64 pkg: go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc cpu: Intel(R) Core(TM) i7-8550U CPU @ 1.80GHz │ inst-otlptracegrpc.bmark.result │ │ sec/op │ ExporterExportSpans/Observability-8 144.3µ ± 4% ExporterExportSpans/NoObservability-8 147.3µ ± 4% geomean 145.8µ │ inst-otlptracegrpc.bmark.result │ │ B/op │ ExporterExportSpans/Observability-8 23.07Ki ± 0% ExporterExportSpans/NoObservability-8 22.34Ki ± 0% geomean 22.70Ki │ inst-otlptracegrpc.bmark.result │ │ allocs/op │ ExporterExportSpans/Observability-8 335.0 ± 0% ExporterExportSpans/NoObservability-8 331.0 ± 0% geomean 333.0 ```
1 parent ce38247 commit c692bc4

File tree

7 files changed

+307
-16
lines changed

7 files changed

+307
-16
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1717
- Add experimental observability for the prometheus exporter in `go.opentelemetry.io/otel/exporters/prometheus`.
1818
Check the `go.opentelemetry.io/otel/exporters/prometheus/internal/x` package documentation for more information. (#7345)
1919
- Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc`. (#7353)
20+
- Add experimental observability metrics in `go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc`. (#7459)
2021

2122
### Fixed
2223

exporters/otlp/otlptrace/otlptracegrpc/client.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919

2020
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
2121
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal"
22+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/counter"
23+
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/observ"
2224
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/otlpconfig"
2325
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/retry"
2426
)
@@ -44,6 +46,9 @@ type client struct {
4446
conn *grpc.ClientConn
4547
tscMu sync.RWMutex
4648
tsc coltracepb.TraceServiceClient
49+
50+
instID int64
51+
inst *observ.Instrumentation
4752
}
4853

4954
// Compile time check *client implements otlptrace.Client.
@@ -67,6 +72,7 @@ func newClient(opts ...Option) *client {
6772
stopCtx: ctx,
6873
stopFunc: cancel,
6974
conn: cfg.GRPCConn,
75+
instID: counter.NextExporterID(),
7076
}
7177

7278
if len(cfg.Traces.Headers) > 0 {
@@ -91,13 +97,24 @@ func (c *client) Start(context.Context) error {
9197
c.conn = conn
9298
}
9399

100+
// Initialize the instrumentation if not already done.
101+
//
102+
// Initialize here instead of NewClient to allow any errors to be passed
103+
// back to the caller and so that any setup of the environment variables to
104+
// enable instrumentation can be set via code.
105+
var err error
106+
if c.inst == nil {
107+
target := c.conn.CanonicalTarget()
108+
c.inst, err = observ.NewInstrumentation(c.instID, target)
109+
}
110+
94111
// The otlptrace.Client interface states this method is called just once,
95112
// so no need to check if already started.
96113
c.tscMu.Lock()
97114
c.tsc = coltracepb.NewTraceServiceClient(c.conn)
98115
c.tscMu.Unlock()
99116

100-
return nil
117+
return err
101118
}
102119

103120
var errAlreadyStopped = errors.New("the client is already stopped")
@@ -188,6 +205,12 @@ func (c *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
188205
ctx, cancel := c.exportContext(ctx)
189206
defer cancel()
190207

208+
var code codes.Code
209+
if c.inst != nil {
210+
op := c.inst.ExportSpans(ctx, len(protoSpans))
211+
defer func() { op.End(uploadErr, code) }()
212+
}
213+
191214
return c.requestFunc(ctx, func(iCtx context.Context) error {
192215
resp, err := c.tsc.Export(iCtx, &coltracepb.ExportTraceServiceRequest{
193216
ResourceSpans: protoSpans,
@@ -201,7 +224,8 @@ func (c *client) UploadTraces(ctx context.Context, protoSpans []*tracepb.Resourc
201224
}
202225
}
203226
// nil is converted to OK.
204-
if status.Code(err) == codes.OK {
227+
code = status.Code(err)
228+
if code == codes.OK {
205229
// Success.
206230
return uploadErr
207231
}

exporters/otlp/otlptrace/otlptracegrpc/client_test.go

Lines changed: 169 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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

3645
func TestMain(m *testing.M) {
@@ -115,7 +124,7 @@ func TestWithEndpointURL(t *testing.T) {
115124
}
116125

117126
func 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+
}

exporters/otlp/otlptrace/otlptracegrpc/internal/counter/counter.go

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

exporters/otlp/otlptrace/otlptracegrpc/internal/counter/counter_test.go

Lines changed: 65 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

exporters/otlp/otlptrace/otlptracegrpc/internal/gen.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,6 @@ package internal // import "go.opentelemetry.io/otel/exporters/otlp/otlptrace/ot
2929

3030
//go:generate gotmpl --body=../../../../../internal/shared/otlp/observ/target.go.tmpl "--data={ \"pkg\": \"observ\", \"pkg_path\": \"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/observ\" }" --out=observ/target.go
3131
//go:generate gotmpl --body=../../../../../internal/shared/otlp/observ/target_test.go.tmpl "--data={ \"pkg\": \"observ\" }" --out=observ/target_test.go
32+
33+
//go:generate gotmpl --body=../../../../../internal/shared/counter/counter.go.tmpl "--data={ \"pkg\": \"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc/internal/counter\" }" --out=counter/counter.go
34+
//go:generate gotmpl --body=../../../../../internal/shared/counter/counter_test.go.tmpl "--data={}" --out=counter/counter_test.go

0 commit comments

Comments
 (0)