1
0
mirror of https://github.com/open-telemetry/opentelemetry-go.git synced 2024-12-30 21:20:04 +02:00

Split connection management away from exporter (#1369)

* Split protocol handling away from exporter

This commits adds a ProtocolDriver interface, which the exporter
will use to connect to the collector and send both metrics and traces
to it. That way, the Exporter type is free from dealing with any
connection/protocol details, as this business is taken over by the
implementations of the ProtocolDriver interface.

The gRPC code from the exporter is moved into the implementation of
ProtocolDriver. Currently it only maintains a single connection,
just as the Exporter used to do.

With the split, most of the Exporter options became actually gRPC
connection manager's options. Currently the only option that remained
to be Exporter's is about setting the export kind selector.

* Update changelog

* Increase the test coverage of GRPC driver

* Do not close a channel with multiple senders

The disconnected channel can be used for sending by multiple
goroutines (for example, by metric controller and span processor), so
this channel should not be closed at all. Dropping this line closes a
race between closing a channel and sending to it.

* Simplify new connection handler

The callbacks never return an error, so drop the return type from it.

* Access clients under a lock

The client may change as a result on reconnection in background, so
guard against a racy access.

* Simplify the GRPC driver a bit

The config type was exported earlier to have a consistent way of
configuring the driver, when also the multiple connection driver would
appear. Since we are not going to add a multiple connection driver,
pass the options directly to the driver constructor. Also shorten the
name of the constructor to `NewGRPCDriver`.

* Merge common gRPC code back into the driver

The common code was supposed to be shared between single connection
driver and multiple connection driver, but since the latter won't be
happening, it makes no sense to keep the not-so-common code in a
separate file. Also drop some abstraction too.

* Rename the file with gRPC driver implementation

* Update changelog

* Sleep for a second to trigger the timeout

Sometimes CI has it's better moments, so it's blazing fast and manages
to finish shutting the exporter down within the 1 microsecond timeout.

* Increase the timeout for shutting down the exporter

One millisecond is quite short, and I was getting failures locally or
in CI:

go test ./... + race in ./exporters/otlp
2020/12/14 18:27:54 rpc error: code = Canceled desc = context canceled
2020/12/14 18:27:54 context deadline exceeded
--- FAIL: TestNewExporter_withMultipleAttributeTypes (0.37s)
    otlp_integration_test.go:541: resource span count: got 0, want 1
FAIL
FAIL	go.opentelemetry.io/otel/exporters/otlp	5.278s

or

go test ./... + coverage in ./exporters/otlp
2020/12/14 17:41:16 rpc error: code = Canceled desc = context canceled
2020/12/14 17:41:16 exporter disconnected
--- FAIL: TestNewExporter_endToEnd (1.53s)
    --- FAIL: TestNewExporter_endToEnd/WithCompressor (0.41s)
        otlp_integration_test.go:246: span counts: got 3, want 4
2020/12/14 17:41:18 context canceled
FAIL
coverage: 35.3% of statements in ./...
FAIL	go.opentelemetry.io/otel/exporters/otlp	4.753s

* Shut down the providers in end to end test

This is to make sure that all batched spans are actually flushed
before closing the exporter.
This commit is contained in:
Krzesimir Nowak 2020-12-21 21:49:45 +01:00 committed by GitHub
parent add9d933f6
commit 35215264dc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 682 additions and 420 deletions

View File

@ -11,6 +11,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
### Added
- Add the `ReadOnlySpan` and `ReadWriteSpan` interfaces to provide better control for accessing span data. (#1360)
- `NewGRPCDriver` function returns a `ProtocolDriver` that maintains a single gRPC connection to the collector. (#1369)
### Changed
@ -19,6 +20,9 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
- Improve span duration accuracy. (#1360)
- Migrated CI/CD from CircleCI to GitHub Actions (#1382)
- Remove duplicate checkout from GitHub Actions workflow (#1407)
- `NewExporter` from `exporters/otlp` now takes a `ProtocolDriver` as a parameter. (#1369)
- Many OTLP Exporter options became gRPC ProtocolDriver options. (#1369)
### Removed
- Remove `errUninitializedSpan` as its only usage is now obsolete. (#1360)

View File

@ -49,11 +49,12 @@ func initProvider() func() {
// `localhost:30080` address. Otherwise, replace `localhost` with the
// address of your cluster. If you run the app inside k8s, then you can
// probably connect directly to the service through dns
exp, err := otlp.NewExporter(ctx,
driver := otlp.NewGRPCDriver(
otlp.WithInsecure(),
otlp.WithAddress("localhost:30080"),
otlp.WithGRPCDialOption(grpc.WithBlock()), // useful for testing
)
exp, err := otlp.NewExporter(ctx, driver)
handleErr(err, "failed to create exporter")
res, err := resource.New(ctx,

View File

@ -29,7 +29,8 @@ import (
func Example_insecure() {
ctx := context.Background()
exp, err := otlp.NewExporter(ctx, otlp.WithInsecure())
driver := otlp.NewGRPCDriver(otlp.WithInsecure())
exp, err := otlp.NewExporter(ctx, driver)
if err != nil {
log.Fatalf("Failed to create the collector exporter: %v", err)
}
@ -74,7 +75,8 @@ func Example_withTLS() {
}
ctx := context.Background()
exp, err := otlp.NewExporter(ctx, otlp.WithTLSCredentials(creds))
driver := otlp.NewGRPCDriver(otlp.WithTLSCredentials(creds))
exp, err := otlp.NewExporter(ctx, driver)
if err != nil {
log.Fatalf("failed to create the collector exporter: %v", err)
}

View File

@ -16,7 +16,6 @@ package otlp // import "go.opentelemetry.io/otel/exporters/otlp"
import (
"context"
"fmt"
"math/rand"
"sync"
"sync/atomic"
@ -37,9 +36,9 @@ type grpcConnection struct {
cc *grpc.ClientConn
// these fields are read-only after constructor is finished
c config
c grpcConnectionConfig
metadata metadata.MD
newConnectionHandler func(cc *grpc.ClientConn) error
newConnectionHandler func(cc *grpc.ClientConn)
// these channels are created once
disconnectedCh chan bool
@ -52,12 +51,9 @@ type grpcConnection struct {
closeBackgroundConnectionDoneCh func(ch chan struct{})
}
func newGRPCConnection(c config, handler func(cc *grpc.ClientConn) error) *grpcConnection {
func newGRPCConnection(c grpcConnectionConfig, handler func(cc *grpc.ClientConn)) *grpcConnection {
conn := new(grpcConnection)
conn.newConnectionHandler = handler
if c.collectorAddr == "" {
c.collectorAddr = fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorPort)
}
conn.c = c
if len(conn.c.headers) > 0 {
conn.metadata = metadata.New(conn.c.headers)
@ -103,7 +99,7 @@ func (oc *grpcConnection) setStateDisconnected(err error) {
case oc.disconnectedCh <- true:
default:
}
_ = oc.newConnectionHandler(nil)
oc.newConnectionHandler(nil)
}
func (oc *grpcConnection) setStateConnected() {
@ -180,7 +176,8 @@ func (oc *grpcConnection) connect(ctx context.Context) error {
return err
}
oc.setConnection(cc)
return oc.newConnectionHandler(cc)
oc.newConnectionHandler(cc)
return nil
}
// setConnection sets cc as the client connection and returns true if
@ -245,8 +242,6 @@ func (oc *grpcConnection) shutdown(ctx context.Context) error {
return ctx.Err()
}
close(oc.disconnectedCh)
oc.mu.Lock()
cc := oc.cc
oc.cc = nil

View File

@ -0,0 +1,144 @@
// 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 otlp // import "go.opentelemetry.io/otel/exporters/otlp"
import (
"context"
"fmt"
"sync"
"google.golang.org/grpc"
colmetricpb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/collector/metrics/v1"
coltracepb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/collector/trace/v1"
metricpb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/metrics/v1"
tracepb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/trace/v1"
"go.opentelemetry.io/otel/exporters/otlp/internal/transform"
metricsdk "go.opentelemetry.io/otel/sdk/export/metric"
tracesdk "go.opentelemetry.io/otel/sdk/export/trace"
)
type grpcDriver struct {
connection *grpcConnection
lock sync.Mutex
metricsClient colmetricpb.MetricsServiceClient
tracesClient coltracepb.TraceServiceClient
}
func NewGRPCDriver(opts ...GRPCConnectionOption) ProtocolDriver {
cfg := grpcConnectionConfig{
collectorAddr: fmt.Sprintf("%s:%d", DefaultCollectorHost, DefaultCollectorPort),
grpcServiceConfig: DefaultGRPCServiceConfig,
}
for _, opt := range opts {
opt(&cfg)
}
d := &grpcDriver{}
d.connection = newGRPCConnection(cfg, d.handleNewConnection)
return d
}
func (d *grpcDriver) handleNewConnection(cc *grpc.ClientConn) {
d.lock.Lock()
defer d.lock.Unlock()
if cc != nil {
d.metricsClient = colmetricpb.NewMetricsServiceClient(cc)
d.tracesClient = coltracepb.NewTraceServiceClient(cc)
} else {
d.metricsClient = nil
d.tracesClient = nil
}
}
func (d *grpcDriver) Start(ctx context.Context) error {
d.connection.startConnection(ctx)
return nil
}
func (d *grpcDriver) Stop(ctx context.Context) error {
return d.connection.shutdown(ctx)
}
func (d *grpcDriver) ExportMetrics(ctx context.Context, cps metricsdk.CheckpointSet, selector metricsdk.ExportKindSelector) error {
if !d.connection.connected() {
return errDisconnected
}
ctx, cancel := d.connection.contextWithStop(ctx)
defer cancel()
rms, err := transform.CheckpointSet(ctx, selector, cps, 1)
if err != nil {
return err
}
if len(rms) == 0 {
return nil
}
return d.uploadMetrics(ctx, rms)
}
func (d *grpcDriver) uploadMetrics(ctx context.Context, protoMetrics []*metricpb.ResourceMetrics) error {
ctx = d.connection.contextWithMetadata(ctx)
err := func() error {
d.lock.Lock()
defer d.lock.Unlock()
if d.metricsClient == nil {
return errNoClient
}
_, err := d.metricsClient.Export(ctx, &colmetricpb.ExportMetricsServiceRequest{
ResourceMetrics: protoMetrics,
})
return err
}()
if err != nil {
d.connection.setStateDisconnected(err)
}
return err
}
func (d *grpcDriver) ExportTraces(ctx context.Context, ss []*tracesdk.SpanSnapshot) error {
if !d.connection.connected() {
return errDisconnected
}
ctx, cancel := d.connection.contextWithStop(ctx)
defer cancel()
protoSpans := transform.SpanData(ss)
if len(protoSpans) == 0 {
return nil
}
return d.uploadTraces(ctx, protoSpans)
}
func (d *grpcDriver) uploadTraces(ctx context.Context, protoSpans []*tracepb.ResourceSpans) error {
ctx = d.connection.contextWithMetadata(ctx)
err := func() error {
d.lock.Lock()
defer d.lock.Unlock()
if d.tracesClient == nil {
return errNoClient
}
_, err := d.tracesClient.Export(ctx, &coltracepb.ExportTraceServiceRequest{
ResourceSpans: protoSpans,
})
return err
}()
if err != nil {
d.connection.setStateDisconnected(err)
}
return err
}

View File

@ -0,0 +1,145 @@
// 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 otlp // import "go.opentelemetry.io/otel/exporters/otlp"
import (
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
const (
// DefaultGRPCServiceConfig is the gRPC service config used if none is
// provided by the user.
//
// For more info on gRPC service configs:
// https://github.com/grpc/proposal/blob/master/A6-client-retries.md
//
// For more info on the RetryableStatusCodes we allow here:
// https://github.com/open-telemetry/oteps/blob/be2a3fcbaa417ebbf5845cd485d34fdf0ab4a2a4/text/0035-opentelemetry-protocol.md#export-response
//
// Note: MaxAttempts > 5 are treated as 5. See
// https://github.com/grpc/proposal/blob/master/A6-client-retries.md#validation-of-retrypolicy
// for more details.
DefaultGRPCServiceConfig = `{
"methodConfig":[{
"name":[
{ "service":"opentelemetry.proto.collector.metrics.v1.MetricsService" },
{ "service":"opentelemetry.proto.collector.trace.v1.TraceService" }
],
"retryPolicy":{
"MaxAttempts":5,
"InitialBackoff":"0.3s",
"MaxBackoff":"5s",
"BackoffMultiplier":2,
"RetryableStatusCodes":[
"UNAVAILABLE",
"CANCELLED",
"DEADLINE_EXCEEDED",
"RESOURCE_EXHAUSTED",
"ABORTED",
"OUT_OF_RANGE",
"UNAVAILABLE",
"DATA_LOSS"
]
}
}]
}`
)
type grpcConnectionConfig struct {
canDialInsecure bool
collectorAddr string
compressor string
reconnectionPeriod time.Duration
grpcServiceConfig string
grpcDialOptions []grpc.DialOption
headers map[string]string
clientCredentials credentials.TransportCredentials
}
type GRPCConnectionOption func(cfg *grpcConnectionConfig)
// WithInsecure disables client transport security for the exporter's gRPC connection
// just like grpc.WithInsecure() https://pkg.go.dev/google.golang.org/grpc#WithInsecure
// does. Note, by default, client security is required unless WithInsecure is used.
func WithInsecure() GRPCConnectionOption {
return func(cfg *grpcConnectionConfig) {
cfg.canDialInsecure = true
}
}
// WithAddress allows one to set the address that the exporter will
// connect to the collector on. If unset, it will instead try to use
// connect to DefaultCollectorHost:DefaultCollectorPort.
func WithAddress(addr string) GRPCConnectionOption {
return func(cfg *grpcConnectionConfig) {
cfg.collectorAddr = addr
}
}
// WithReconnectionPeriod allows one to set the delay between next connection attempt
// after failing to connect with the collector.
func WithReconnectionPeriod(rp time.Duration) GRPCConnectionOption {
return func(cfg *grpcConnectionConfig) {
cfg.reconnectionPeriod = rp
}
}
// WithCompressor will set the compressor for the gRPC client to use when sending requests.
// It is the responsibility of the caller to ensure that the compressor set has been registered
// with google.golang.org/grpc/encoding. This can be done by encoding.RegisterCompressor. Some
// compressors auto-register on import, such as gzip, which can be registered by calling
// `import _ "google.golang.org/grpc/encoding/gzip"`
func WithCompressor(compressor string) GRPCConnectionOption {
return func(cfg *grpcConnectionConfig) {
cfg.compressor = compressor
}
}
// WithHeaders will send the provided headers with gRPC requests
func WithHeaders(headers map[string]string) GRPCConnectionOption {
return func(cfg *grpcConnectionConfig) {
cfg.headers = headers
}
}
// WithTLSCredentials allows the connection to use TLS credentials
// when talking to the server. It takes in grpc.TransportCredentials instead
// of say a Certificate file or a tls.Certificate, because the retrieving
// these credentials can be done in many ways e.g. plain file, in code tls.Config
// or by certificate rotation, so it is up to the caller to decide what to use.
func WithTLSCredentials(creds credentials.TransportCredentials) GRPCConnectionOption {
return func(cfg *grpcConnectionConfig) {
cfg.clientCredentials = creds
}
}
// WithGRPCServiceConfig defines the default gRPC service config used.
func WithGRPCServiceConfig(serviceConfig string) GRPCConnectionOption {
return func(cfg *grpcConnectionConfig) {
cfg.grpcServiceConfig = serviceConfig
}
}
// WithGRPCDialOption opens support to any grpc.DialOption to be used. If it conflicts
// with some other configuration the GRPC specified via the collector the ones here will
// take preference since they are set last.
func WithGRPCDialOption(opts ...grpc.DialOption) GRPCConnectionOption {
return func(cfg *grpcConnectionConfig) {
cfg.grpcDialOptions = opts
}
}

View File

@ -15,11 +15,6 @@
package otlp // import "go.opentelemetry.io/otel/exporters/otlp"
import (
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
metricsdk "go.opentelemetry.io/otel/sdk/export/metric"
)
@ -30,132 +25,19 @@ const (
// DefaultCollectorHost is the host address the Exporter will attempt
// connect to if no collector address is provided.
DefaultCollectorHost string = "localhost"
// DefaultGRPCServiceConfig is the gRPC service config used if none is
// provided by the user.
//
// For more info on gRPC service configs:
// https://github.com/grpc/proposal/blob/master/A6-client-retries.md
//
// For more info on the RetryableStatusCodes we allow here:
// https://github.com/open-telemetry/oteps/blob/be2a3fcbaa417ebbf5845cd485d34fdf0ab4a2a4/text/0035-opentelemetry-protocol.md#export-response
//
// Note: MaxAttempts > 5 are treated as 5. See
// https://github.com/grpc/proposal/blob/master/A6-client-retries.md#validation-of-retrypolicy
// for more details.
DefaultGRPCServiceConfig = `{
"methodConfig":[{
"name":[
{ "service":"opentelemetry.proto.collector.metrics.v1.MetricsService" },
{ "service":"opentelemetry.proto.collector.trace.v1.TraceService" }
],
"retryPolicy":{
"MaxAttempts":5,
"InitialBackoff":"0.3s",
"MaxBackoff":"5s",
"BackoffMultiplier":2,
"RetryableStatusCodes":[
"UNAVAILABLE",
"CANCELLED",
"DEADLINE_EXCEEDED",
"RESOURCE_EXHAUSTED",
"ABORTED",
"OUT_OF_RANGE",
"UNAVAILABLE",
"DATA_LOSS"
]
}
}]
}`
)
// ExporterOption are setting options passed to an Exporter on creation.
type ExporterOption func(*config)
type config struct {
canDialInsecure bool
collectorAddr string
compressor string
reconnectionPeriod time.Duration
grpcServiceConfig string
grpcDialOptions []grpc.DialOption
headers map[string]string
clientCredentials credentials.TransportCredentials
exportKindSelector metricsdk.ExportKindSelector
}
// WithInsecure disables client transport security for the exporter's gRPC connection
// just like grpc.WithInsecure() https://pkg.go.dev/google.golang.org/grpc#WithInsecure
// does. Note, by default, client security is required unless WithInsecure is used.
func WithInsecure() ExporterOption {
return func(cfg *config) {
cfg.canDialInsecure = true
}
}
// WithAddress allows one to set the address that the exporter will
// connect to the collector on. If unset, it will instead try to use
// connect to DefaultCollectorHost:DefaultCollectorPort.
func WithAddress(addr string) ExporterOption {
return func(cfg *config) {
cfg.collectorAddr = addr
}
}
// WithReconnectionPeriod allows one to set the delay between next connection attempt
// after failing to connect with the collector.
func WithReconnectionPeriod(rp time.Duration) ExporterOption {
return func(cfg *config) {
cfg.reconnectionPeriod = rp
}
}
// WithCompressor will set the compressor for the gRPC client to use when sending requests.
// It is the responsibility of the caller to ensure that the compressor set has been registered
// with google.golang.org/grpc/encoding. This can be done by encoding.RegisterCompressor. Some
// compressors auto-register on import, such as gzip, which can be registered by calling
// `import _ "google.golang.org/grpc/encoding/gzip"`
func WithCompressor(compressor string) ExporterOption {
return func(cfg *config) {
cfg.compressor = compressor
}
}
// WithHeaders will send the provided headers with gRPC requests
func WithHeaders(headers map[string]string) ExporterOption {
return func(cfg *config) {
cfg.headers = headers
}
}
// WithTLSCredentials allows the connection to use TLS credentials
// when talking to the server. It takes in grpc.TransportCredentials instead
// of say a Certificate file or a tls.Certificate, because the retrieving
// these credentials can be done in many ways e.g. plain file, in code tls.Config
// or by certificate rotation, so it is up to the caller to decide what to use.
func WithTLSCredentials(creds credentials.TransportCredentials) ExporterOption {
return func(cfg *config) {
cfg.clientCredentials = creds
}
}
// WithGRPCServiceConfig defines the default gRPC service config used.
func WithGRPCServiceConfig(serviceConfig string) ExporterOption {
return func(cfg *config) {
cfg.grpcServiceConfig = serviceConfig
}
}
// WithGRPCDialOption opens support to any grpc.DialOption to be used. If it conflicts
// with some other configuration the GRPC specified via the collector the ones here will
// take preference since they are set last.
func WithGRPCDialOption(opts ...grpc.DialOption) ExporterOption {
return func(cfg *config) {
cfg.grpcDialOptions = opts
}
}
// WithMetricExportKindSelector defines the ExportKindSelector used for selecting
// AggregationTemporality (i.e., Cumulative vs. Delta aggregation).
// WithMetricExportKindSelector defines the ExportKindSelector used
// for selecting AggregationTemporality (i.e., Cumulative vs. Delta
// aggregation). If not specified otherwise, exporter will use a
// cumulative export kind selector.
func WithMetricExportKindSelector(selector metricsdk.ExportKindSelector) ExporterOption {
return func(cfg *config) {
cfg.exportKindSelector = selector

View File

@ -14,19 +14,11 @@
package otlp // import "go.opentelemetry.io/otel/exporters/otlp"
// This code was based on
// contrib.go.opencensus.io/exporter/ocagent/connection.go
import (
"context"
"errors"
"sync"
"google.golang.org/grpc"
colmetricpb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/collector/metrics/v1"
coltracepb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/collector/trace/v1"
"go.opentelemetry.io/otel/exporters/otlp/internal/transform"
"go.opentelemetry.io/otel/metric"
metricsdk "go.opentelemetry.io/otel/sdk/export/metric"
"go.opentelemetry.io/otel/sdk/export/metric/aggregation"
@ -37,30 +29,31 @@ import (
// from OpenTelemetry instrumented to code using OpenTelemetry protocol
// buffers to a configurable receiver.
type Exporter struct {
// mu protects the non-atomic and non-channel variables
mu sync.RWMutex
// senderMu protects the concurrent unsafe sends on the shared gRPC client connection.
senderMu sync.Mutex
started bool
traceExporter coltracepb.TraceServiceClient
metricExporter colmetricpb.MetricsServiceClient
cc *grpcConnection
cfg config
driver ProtocolDriver
mu sync.RWMutex
started bool
startOnce sync.Once
stopOnce sync.Once
exportKindSelector metricsdk.ExportKindSelector
}
var _ tracesdk.SpanExporter = (*Exporter)(nil)
var _ metricsdk.Exporter = (*Exporter)(nil)
// newConfig initializes a config struct with default values and applies
// any ExporterOptions provided.
func newConfig(opts ...ExporterOption) config {
cfg := config{
grpcServiceConfig: DefaultGRPCServiceConfig,
// NewExporter constructs a new Exporter and starts it.
func NewExporter(ctx context.Context, driver ProtocolDriver, opts ...ExporterOption) (*Exporter, error) {
exp := NewUnstartedExporter(driver, opts...)
if err := exp.Start(ctx); err != nil {
return nil, err
}
return exp, nil
}
// NewUnstartedExporter constructs a new Exporter and does not start it.
func NewUnstartedExporter(driver ProtocolDriver, opts ...ExporterOption) *Exporter {
cfg := config{
// Note: the default ExportKindSelector is specified
// as Cumulative:
// https://github.com/open-telemetry/opentelemetry-specification/issues/731
@ -69,38 +62,10 @@ func newConfig(opts ...ExporterOption) config {
for _, opt := range opts {
opt(&cfg)
}
return cfg
}
// NewExporter constructs a new Exporter and starts it.
func NewExporter(ctx context.Context, opts ...ExporterOption) (*Exporter, error) {
exp := NewUnstartedExporter(opts...)
if err := exp.Start(ctx); err != nil {
return nil, err
return &Exporter{
cfg: cfg,
driver: driver,
}
return exp, nil
}
// NewUnstartedExporter constructs a new Exporter and does not start it.
func NewUnstartedExporter(opts ...ExporterOption) *Exporter {
e := new(Exporter)
cfg := newConfig(opts...)
e.exportKindSelector = cfg.exportKindSelector
e.cc = newGRPCConnection(cfg, e.handleNewConnection)
return e
}
func (e *Exporter) handleNewConnection(cc *grpc.ClientConn) error {
e.mu.Lock()
defer e.mu.Unlock()
if cc != nil {
e.metricExporter = colmetricpb.NewMetricsServiceClient(cc)
e.traceExporter = coltracepb.NewTraceServiceClient(cc)
} else {
e.metricExporter = nil
e.traceExporter = nil
}
return nil
}
var (
@ -109,30 +74,26 @@ var (
errDisconnected = errors.New("exporter disconnected")
)
// Start dials to the collector, establishing a connection to it. It also
// initiates the Config and Trace services by sending over the initial
// messages that consist of the node identifier. Start invokes a background
// connector that will reattempt connections to the collector periodically
// if the connection dies.
// Start establishes connections to the OpenTelemetry collector. Starting an
// already started exporter returns an error.
func (e *Exporter) Start(ctx context.Context) error {
var err = errAlreadyStarted
e.startOnce.Do(func() {
e.mu.Lock()
e.started = true
e.mu.Unlock()
err = nil
e.cc.startConnection(ctx)
err = e.driver.Start(ctx)
})
return err
}
// Shutdown closes all connections and releases resources currently being used
// by the exporter. If the exporter is not started this does nothing.
// by the exporter. If the exporter is not started this does nothing. A shut
// down exporter can't be started again. Shutting down an already shut down
// exporter does nothing.
func (e *Exporter) Shutdown(ctx context.Context) error {
e.mu.RLock()
cc := e.cc
started := e.started
e.mu.RUnlock()
@ -143,10 +104,7 @@ func (e *Exporter) Shutdown(ctx context.Context) error {
var err error
e.stopOnce.Do(func() {
// Clean things up before checking this error.
err = cc.shutdown(ctx)
// At this point we can change the state variable started
err = e.driver.Stop(ctx)
e.mu.Lock()
e.started = false
e.mu.Unlock()
@ -159,75 +117,19 @@ func (e *Exporter) Shutdown(ctx context.Context) error {
// interface. It transforms and batches metric Records into OTLP Metrics and
// transmits them to the configured collector.
func (e *Exporter) Export(parent context.Context, cps metricsdk.CheckpointSet) error {
ctx, cancel := e.cc.contextWithStop(parent)
defer cancel()
// Hardcode the number of worker goroutines to 1. We later will
// need to see if there's a way to adjust that number for longer
// running operations.
rms, err := transform.CheckpointSet(ctx, e, cps, 1)
if err != nil {
return err
}
if !e.cc.connected() {
return errDisconnected
}
err = func() error {
e.senderMu.Lock()
defer e.senderMu.Unlock()
if e.metricExporter == nil {
return errNoClient
}
_, err := e.metricExporter.Export(e.cc.contextWithMetadata(ctx), &colmetricpb.ExportMetricsServiceRequest{
ResourceMetrics: rms,
})
return err
}()
if err != nil {
e.cc.setStateDisconnected(err)
}
return err
return e.driver.ExportMetrics(parent, cps, e.cfg.exportKindSelector)
}
// ExportKindFor reports back to the OpenTelemetry SDK sending this Exporter
// metric telemetry that it needs to be provided in a cumulative format.
// metric telemetry that it needs to be provided in a configured format.
func (e *Exporter) ExportKindFor(desc *metric.Descriptor, kind aggregation.Kind) metricsdk.ExportKind {
return e.exportKindSelector.ExportKindFor(desc, kind)
return e.cfg.exportKindSelector.ExportKindFor(desc, kind)
}
// ExportSpans exports a batch of SpanSnapshot.
// ExportSpans implements the
// "go.opentelemetry.io/otel/sdk/export/trace".SpanExporter interface. It
// transforms and batches trace SpanSnapshots into OTLP Trace and transmits them
// to the configured collector.
func (e *Exporter) ExportSpans(ctx context.Context, ss []*tracesdk.SpanSnapshot) error {
return e.uploadTraces(ctx, ss)
}
func (e *Exporter) uploadTraces(ctx context.Context, ss []*tracesdk.SpanSnapshot) error {
ctx, cancel := e.cc.contextWithStop(ctx)
defer cancel()
if !e.cc.connected() {
return nil
}
protoSpans := transform.SpanData(ss)
if len(protoSpans) == 0 {
return nil
}
err := func() error {
e.senderMu.Lock()
defer e.senderMu.Unlock()
if e.traceExporter == nil {
return errNoClient
}
_, err := e.traceExporter.Export(e.cc.contextWithMetadata(ctx), &coltracepb.ExportTraceServiceRequest{
ResourceSpans: protoSpans,
})
return err
}()
if err != nil {
e.cc.setStateDisconnected(err)
}
return err
return e.driver.ExportTraces(ctx, ss)
}

View File

@ -25,28 +25,53 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/grpc"
"google.golang.org/grpc/encoding/gzip"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/exporters/otlp"
commonpb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/common/v1"
"go.opentelemetry.io/otel/label"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/metric/number"
metricsdk "go.opentelemetry.io/otel/sdk/export/metric"
exportmetric "go.opentelemetry.io/otel/sdk/export/metric"
exporttrace "go.opentelemetry.io/otel/sdk/export/trace"
"go.opentelemetry.io/otel/sdk/instrumentation"
"go.opentelemetry.io/otel/sdk/metric/aggregator/sum"
"go.opentelemetry.io/otel/sdk/metric/controller/push"
processor "go.opentelemetry.io/otel/sdk/metric/processor/basic"
"go.opentelemetry.io/otel/sdk/metric/selector/simple"
"go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
)
func TestNewExporter_endToEnd(t *testing.T) {
tests := []struct {
name string
additionalOpts []otlp.ExporterOption
additionalOpts []otlp.GRPCConnectionOption
}{
{
name: "StandardExporter",
},
{
name: "WithCompressor",
additionalOpts: []otlp.GRPCConnectionOption{
otlp.WithCompressor(gzip.Name),
},
},
{
name: "WithGRPCServiceConfig",
additionalOpts: []otlp.GRPCConnectionOption{
otlp.WithGRPCServiceConfig("{}"),
},
},
{
name: "WithGRPCDialOptions",
additionalOpts: []otlp.GRPCConnectionOption{
otlp.WithGRPCDialOption(grpc.WithBlock()),
},
},
}
for _, test := range tests {
@ -56,7 +81,23 @@ func TestNewExporter_endToEnd(t *testing.T) {
}
}
func newExporterEndToEndTest(t *testing.T, additionalOpts []otlp.ExporterOption) {
func newGRPCExporter(t *testing.T, ctx context.Context, address string, additionalOpts ...otlp.GRPCConnectionOption) *otlp.Exporter {
opts := []otlp.GRPCConnectionOption{
otlp.WithInsecure(),
otlp.WithAddress(address),
otlp.WithReconnectionPeriod(50 * time.Millisecond),
}
opts = append(opts, additionalOpts...)
driver := otlp.NewGRPCDriver(opts...)
exp, err := otlp.NewExporter(ctx, driver)
if err != nil {
t.Fatalf("failed to create a new collector exporter: %v", err)
}
return exp
}
func newExporterEndToEndTest(t *testing.T, additionalOpts []otlp.GRPCConnectionOption) {
mc := runMockColAtAddr(t, "localhost:56561")
defer func() {
@ -65,20 +106,10 @@ func newExporterEndToEndTest(t *testing.T, additionalOpts []otlp.ExporterOption)
<-time.After(5 * time.Millisecond)
opts := []otlp.ExporterOption{
otlp.WithInsecure(),
otlp.WithAddress(mc.address),
otlp.WithReconnectionPeriod(50 * time.Millisecond),
}
opts = append(opts, additionalOpts...)
ctx := context.Background()
exp, err := otlp.NewExporter(ctx, opts...)
if err != nil {
t.Fatalf("failed to create a new collector exporter: %v", err)
}
exp := newGRPCExporter(t, ctx, mc.address, additionalOpts...)
defer func() {
ctx, cancel := context.WithTimeout(ctx, time.Second)
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := exp.Shutdown(ctx); err != nil {
panic(err)
@ -121,7 +152,7 @@ func newExporterEndToEndTest(t *testing.T, additionalOpts []otlp.ExporterOption)
}
selector := simple.NewWithInexpensiveDistribution()
processor := processor.New(selector, metricsdk.StatelessExportKindSelector())
processor := processor.New(selector, exportmetric.StatelessExportKindSelector())
pusher := push.New(processor, exp)
pusher.Start()
@ -185,12 +216,22 @@ func newExporterEndToEndTest(t *testing.T, additionalOpts []otlp.ExporterOption)
// Flush and close.
pusher.Stop()
func() {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := tp1.Shutdown(ctx); err != nil {
t.Fatalf("failed to shut down a tracer provider 1: %v", err)
}
if err := tp2.Shutdown(ctx); err != nil {
t.Fatalf("failed to shut down a tracer provider 2: %v", err)
}
}()
// Wait >2 cycles.
<-time.After(40 * time.Millisecond)
// Now shutdown the exporter
ctx, cancel := context.WithTimeout(ctx, time.Millisecond)
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := exp.Shutdown(ctx); err != nil {
t.Fatalf("failed to stop the exporter: %v", err)
@ -308,13 +349,7 @@ func TestNewExporter_invokeStartThenStopManyTimes(t *testing.T) {
}()
ctx := context.Background()
exp, err := otlp.NewExporter(ctx,
otlp.WithInsecure(),
otlp.WithReconnectionPeriod(50*time.Millisecond),
otlp.WithAddress(mc.address))
if err != nil {
t.Fatalf("error creating exporter: %v", err)
}
exp := newGRPCExporter(t, ctx, mc.address)
defer func() {
if err := exp.Shutdown(ctx); err != nil {
panic(err)
@ -344,13 +379,8 @@ func TestNewExporter_collectorConnectionDiesThenReconnects(t *testing.T) {
reconnectionPeriod := 20 * time.Millisecond
ctx := context.Background()
exp, err := otlp.NewExporter(ctx,
otlp.WithInsecure(),
otlp.WithAddress(mc.address),
exp := newGRPCExporter(t, ctx, mc.address,
otlp.WithReconnectionPeriod(reconnectionPeriod))
if err != nil {
t.Fatalf("Unexpected error: %v", err)
}
defer func() {
_ = exp.Shutdown(ctx)
}()
@ -417,13 +447,7 @@ func TestNewExporter_collectorOnBadConnection(t *testing.T) {
address := fmt.Sprintf("localhost:%s", collectorPortStr)
ctx := context.Background()
exp, err := otlp.NewExporter(ctx,
otlp.WithInsecure(),
otlp.WithReconnectionPeriod(50*time.Millisecond),
otlp.WithAddress(address))
if err != nil {
t.Fatalf("Despite an indefinite background reconnection, got error: %v", err)
}
exp := newGRPCExporter(t, ctx, address)
_ = exp.Shutdown(ctx)
}
@ -433,19 +457,9 @@ func TestNewExporter_withAddress(t *testing.T) {
_ = mc.stop()
}()
exp := otlp.NewUnstartedExporter(
otlp.WithInsecure(),
otlp.WithReconnectionPeriod(50*time.Millisecond),
otlp.WithAddress(mc.address))
ctx := context.Background()
defer func() {
_ = exp.Shutdown(ctx)
}()
if err := exp.Start(ctx); err != nil {
t.Fatalf("Unexpected Start error: %v", err)
}
exp := newGRPCExporter(t, ctx, mc.address)
_ = exp.Shutdown(ctx)
}
func TestNewExporter_withHeaders(t *testing.T) {
@ -455,12 +469,8 @@ func TestNewExporter_withHeaders(t *testing.T) {
}()
ctx := context.Background()
exp, _ := otlp.NewExporter(ctx,
otlp.WithInsecure(),
otlp.WithReconnectionPeriod(50*time.Millisecond),
otlp.WithAddress(mc.address),
otlp.WithHeaders(map[string]string{"header1": "value1"}),
)
exp := newGRPCExporter(t, ctx, mc.address,
otlp.WithHeaders(map[string]string{"header1": "value1"}))
require.NoError(t, exp.ExportSpans(ctx, []*exporttrace.SpanSnapshot{{Name: "in the midst"}}))
defer func() {
@ -482,11 +492,7 @@ func TestNewExporter_withMultipleAttributeTypes(t *testing.T) {
<-time.After(5 * time.Millisecond)
ctx := context.Background()
exp, _ := otlp.NewExporter(ctx,
otlp.WithInsecure(),
otlp.WithReconnectionPeriod(50*time.Millisecond),
otlp.WithAddress(mc.address),
)
exp := newGRPCExporter(t, ctx, mc.address)
defer func() {
_ = exp.Shutdown(ctx)
@ -517,19 +523,20 @@ func TestNewExporter_withMultipleAttributeTypes(t *testing.T) {
span.SetAttributes(testKvs...)
span.End()
selector := simple.NewWithInexpensiveDistribution()
processor := processor.New(selector, metricsdk.StatelessExportKindSelector())
pusher := push.New(processor, exp)
pusher.Start()
// Flush and close.
pusher.Stop()
func() {
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := tp.Shutdown(ctx); err != nil {
t.Fatalf("failed to shut down a tracer provider: %v", err)
}
}()
// Wait >2 cycles.
<-time.After(40 * time.Millisecond)
// Now shutdown the exporter
ctx, cancel := context.WithTimeout(ctx, time.Millisecond)
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := exp.Shutdown(ctx); err != nil {
t.Fatalf("failed to stop the exporter: %v", err)
@ -623,3 +630,134 @@ func TestNewExporter_withMultipleAttributeTypes(t *testing.T) {
assert.Equal(t, expected[i], actual)
}
}
type discCheckpointSet struct{}
func (discCheckpointSet) ForEach(kindSelector exportmetric.ExportKindSelector, recordFunc func(exportmetric.Record) error) error {
desc := metric.NewDescriptor(
"foo",
metric.CounterInstrumentKind,
number.Int64Kind,
)
res := resource.NewWithAttributes(label.String("a", "b"))
agg := sum.New(1)
start := time.Now().Add(-20 * time.Minute)
end := time.Now()
labels := label.NewSet()
rec := exportmetric.NewRecord(&desc, &labels, res, agg[0].Aggregation(), start, end)
return recordFunc(rec)
}
func (discCheckpointSet) Lock() {}
func (discCheckpointSet) Unlock() {}
func (discCheckpointSet) RLock() {}
func (discCheckpointSet) RUnlock() {}
func discSpanSnapshot() *exporttrace.SpanSnapshot {
return &exporttrace.SpanSnapshot{
SpanContext: trace.SpanContext{
TraceID: trace.TraceID{2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9},
SpanID: trace.SpanID{3, 4, 5, 6, 7, 8, 9, 0},
TraceFlags: trace.FlagsSampled,
},
ParentSpanID: trace.SpanID{1, 2, 3, 4, 5, 6, 7, 8},
SpanKind: trace.SpanKindInternal,
Name: "foo",
StartTime: time.Now().Add(-20 * time.Minute),
EndTime: time.Now(),
Attributes: []label.KeyValue{},
MessageEvents: []exporttrace.Event{},
Links: []trace.Link{},
StatusCode: codes.Ok,
StatusMessage: "",
HasRemoteParent: false,
DroppedAttributeCount: 0,
DroppedMessageEventCount: 0,
DroppedLinkCount: 0,
ChildSpanCount: 0,
Resource: resource.NewWithAttributes(label.String("a", "b")),
InstrumentationLibrary: instrumentation.Library{
Name: "bar",
Version: "0.0.0",
},
}
}
func TestDisconnected(t *testing.T) {
ctx := context.Background()
// The address is whatever, we want to be disconnected. But we
// setting a blocking connection, so dialing to the invalid
// address actually fails.
exp := newGRPCExporter(t, ctx, "invalid",
otlp.WithReconnectionPeriod(time.Hour),
otlp.WithGRPCDialOption(
grpc.WithBlock(),
grpc.FailOnNonTempDialError(true),
),
)
defer func() {
assert.NoError(t, exp.Shutdown(ctx))
}()
assert.Error(t, exp.Export(ctx, discCheckpointSet{}))
assert.Error(t, exp.ExportSpans(ctx, []*exporttrace.SpanSnapshot{discSpanSnapshot()}))
}
type emptyCheckpointSet struct{}
func (emptyCheckpointSet) ForEach(kindSelector exportmetric.ExportKindSelector, recordFunc func(exportmetric.Record) error) error {
return nil
}
func (emptyCheckpointSet) Lock() {}
func (emptyCheckpointSet) Unlock() {}
func (emptyCheckpointSet) RLock() {}
func (emptyCheckpointSet) RUnlock() {}
func TestEmptyData(t *testing.T) {
mc := runMockColAtAddr(t, "localhost:56561")
defer func() {
_ = mc.stop()
}()
<-time.After(5 * time.Millisecond)
ctx := context.Background()
exp := newGRPCExporter(t, ctx, mc.address)
defer func() {
assert.NoError(t, exp.Shutdown(ctx))
}()
assert.NoError(t, exp.ExportSpans(ctx, nil))
assert.NoError(t, exp.Export(ctx, emptyCheckpointSet{}))
}
type failCheckpointSet struct{}
func (failCheckpointSet) ForEach(kindSelector exportmetric.ExportKindSelector, recordFunc func(exportmetric.Record) error) error {
return fmt.Errorf("fail")
}
func (failCheckpointSet) Lock() {}
func (failCheckpointSet) Unlock() {}
func (failCheckpointSet) RLock() {}
func (failCheckpointSet) RUnlock() {}
func TestFailedMetricTransform(t *testing.T) {
mc := runMockColAtAddr(t, "localhost:56561")
defer func() {
_ = mc.stop()
}()
<-time.After(5 * time.Millisecond)
ctx := context.Background()
exp := newGRPCExporter(t, ctx, mc.address)
defer func() {
assert.NoError(t, exp.Shutdown(ctx))
}()
assert.Error(t, exp.Export(ctx, failCheckpointSet{}))
}

View File

@ -23,7 +23,6 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
colmetricpb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/collector/metrics/v1"
commonpb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/common/v1"
metricpb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/metrics/v1"
resourcepb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/resource/v1"
@ -36,8 +35,6 @@ import (
"go.opentelemetry.io/otel/sdk/metric/aggregator/histogram"
"go.opentelemetry.io/otel/sdk/metric/aggregator/sum"
"go.opentelemetry.io/otel/sdk/resource"
"google.golang.org/grpc"
)
var (
@ -55,28 +52,6 @@ func pointTime() uint64 {
return uint64(intervalEnd.UnixNano())
}
type metricsServiceClientStub struct {
rm []metricpb.ResourceMetrics
}
func (m *metricsServiceClientStub) Export(ctx context.Context, in *colmetricpb.ExportMetricsServiceRequest, opts ...grpc.CallOption) (*colmetricpb.ExportMetricsServiceResponse, error) {
for _, rm := range in.GetResourceMetrics() {
if rm == nil {
continue
}
m.rm = append(m.rm, *rm)
}
return &colmetricpb.ExportMetricsServiceResponse{}, nil
}
func (m *metricsServiceClientStub) ResourceMetrics() []metricpb.ResourceMetrics {
return m.rm
}
func (m *metricsServiceClientStub) Reset() {
m.rm = nil
}
type checkpointSet struct {
sync.RWMutex
records []metricsdk.Record
@ -765,12 +740,8 @@ func TestStatelessExportKind(t *testing.T) {
}
}
// What works single-threaded should work multi-threaded
func runMetricExportTests(t *testing.T, opts []ExporterOption, rs []record, expected []metricpb.ResourceMetrics) {
exp := NewUnstartedExporter(opts...)
msc := &metricsServiceClientStub{}
exp.metricExporter = msc
exp.started = true
exp, driver := newExporter(t, opts...)
recs := map[label.Distinct][]metricsdk.Record{}
resources := map[label.Distinct]*resource.Resource{}
@ -830,7 +801,7 @@ func runMetricExportTests(t *testing.T, opts []ExporterOption, rs []record, expe
resource, instrumentationLibrary string
}
got := map[key][]*metricpb.Metric{}
for _, rm := range msc.ResourceMetrics() {
for _, rm := range driver.rm {
for _, ilm := range rm.InstrumentationLibraryMetrics {
k := key{
resource: rm.GetResource().String(),
@ -910,10 +881,7 @@ func runMetricExportTests(t *testing.T, opts []ExporterOption, rs []record, expe
}
func TestEmptyMetricExport(t *testing.T) {
msc := &metricsServiceClientStub{}
exp := NewUnstartedExporter()
exp.metricExporter = msc
exp.started = true
exp, driver := newExporter(t)
for _, test := range []struct {
records []metricsdk.Record
@ -928,8 +896,8 @@ func TestEmptyMetricExport(t *testing.T) {
[]metricpb.ResourceMetrics(nil),
},
} {
msc.Reset()
driver.Reset()
require.NoError(t, exp.Export(context.Background(), &checkpointSet{records: test.records}))
assert.Equal(t, test.want, msc.ResourceMetrics())
assert.Equal(t, test.want, driver.rm)
}
}

View File

@ -20,10 +20,8 @@ import (
"time"
"github.com/stretchr/testify/assert"
"google.golang.org/grpc"
"go.opentelemetry.io/otel/codes"
coltracepb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/collector/trace/v1"
commonpb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/common/v1"
resourcepb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/resource/v1"
tracepb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/trace/v1"
@ -35,33 +33,8 @@ import (
"go.opentelemetry.io/otel/sdk/resource"
)
type traceServiceClientStub struct {
rs []tracepb.ResourceSpans
}
func (t *traceServiceClientStub) Export(ctx context.Context, in *coltracepb.ExportTraceServiceRequest, opts ...grpc.CallOption) (*coltracepb.ExportTraceServiceResponse, error) {
for _, rs := range in.GetResourceSpans() {
if rs == nil {
continue
}
t.rs = append(t.rs, *rs)
}
return &coltracepb.ExportTraceServiceResponse{}, nil
}
func (t *traceServiceClientStub) ResourceSpans() []tracepb.ResourceSpans {
return t.rs
}
func (t *traceServiceClientStub) Reset() {
t.rs = nil
}
func TestExportSpans(t *testing.T) {
tsc := &traceServiceClientStub{}
exp := NewUnstartedExporter()
exp.traceExporter = tsc
exp.started = true
exp, driver := newExporter(t)
// March 31, 2020 5:01:26 1234nanos (UTC)
startTime := time.Unix(1585674086, 1234)
@ -352,8 +325,8 @@ func TestExportSpans(t *testing.T) {
},
},
} {
tsc.Reset()
driver.Reset()
assert.NoError(t, exp.ExportSpans(context.Background(), test.sd))
assert.ElementsMatch(t, test.want, tsc.ResourceSpans())
assert.ElementsMatch(t, test.want, driver.rs)
}
}

View File

@ -20,25 +20,89 @@ import (
"sync"
"testing"
"time"
"github.com/stretchr/testify/require"
metricpb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/metrics/v1"
tracepb "go.opentelemetry.io/otel/exporters/otlp/internal/opentelemetry-proto-gen/trace/v1"
"go.opentelemetry.io/otel/exporters/otlp/internal/transform"
metricsdk "go.opentelemetry.io/otel/sdk/export/metric"
tracesdk "go.opentelemetry.io/otel/sdk/export/trace"
)
type stubProtocolDriver struct {
rm []metricpb.ResourceMetrics
rs []tracepb.ResourceSpans
}
var _ ProtocolDriver = (*stubProtocolDriver)(nil)
func (m *stubProtocolDriver) Start(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return nil
}
}
func (m *stubProtocolDriver) Stop(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
return nil
}
}
func (m *stubProtocolDriver) ExportMetrics(parent context.Context, cps metricsdk.CheckpointSet, selector metricsdk.ExportKindSelector) error {
rms, err := transform.CheckpointSet(parent, selector, cps, 1)
if err != nil {
return err
}
for _, rm := range rms {
if rm == nil {
continue
}
m.rm = append(m.rm, *rm)
}
return nil
}
func (m *stubProtocolDriver) ExportTraces(ctx context.Context, ss []*tracesdk.SpanSnapshot) error {
for _, rs := range transform.SpanData(ss) {
if rs == nil {
continue
}
m.rs = append(m.rs, *rs)
}
return nil
}
func (m *stubProtocolDriver) Reset() {
m.rm = nil
m.rs = nil
}
func newExporter(t *testing.T, opts ...ExporterOption) (*Exporter, *stubProtocolDriver) {
driver := &stubProtocolDriver{}
exp, err := NewExporter(context.Background(), driver, opts...)
require.NoError(t, err)
return exp, driver
}
func TestExporterShutdownHonorsTimeout(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
e := NewUnstartedExporter()
orig := e.cc.closeBackgroundConnectionDoneCh
e.cc.closeBackgroundConnectionDoneCh = func(ch chan struct{}) {
go func() {
<-ctx.Done()
orig(ch)
}()
}
e := NewUnstartedExporter(&stubProtocolDriver{})
if err := e.Start(ctx); err != nil {
t.Fatalf("failed to start exporter: %v", err)
}
innerCtx, innerCancel := context.WithTimeout(ctx, time.Microsecond)
<-time.After(time.Second)
if err := e.Shutdown(innerCtx); err == nil {
t.Error("expected context DeadlineExceeded error, got nil")
} else if !errors.Is(err, context.DeadlineExceeded) {
@ -51,14 +115,7 @@ func TestExporterShutdownHonorsCancel(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
e := NewUnstartedExporter()
orig := e.cc.closeBackgroundConnectionDoneCh
e.cc.closeBackgroundConnectionDoneCh = func(ch chan struct{}) {
go func() {
<-ctx.Done()
orig(ch)
}()
}
e := NewUnstartedExporter(&stubProtocolDriver{})
if err := e.Start(ctx); err != nil {
t.Fatalf("failed to start exporter: %v", err)
}
@ -77,7 +134,7 @@ func TestExporterShutdownNoError(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Minute)
defer cancel()
e := NewUnstartedExporter()
e := NewUnstartedExporter(&stubProtocolDriver{})
if err := e.Start(ctx); err != nil {
t.Fatalf("failed to start exporter: %v", err)
}
@ -89,7 +146,7 @@ func TestExporterShutdownNoError(t *testing.T) {
func TestExporterShutdownManyTimes(t *testing.T) {
ctx := context.Background()
e, err := NewExporter(ctx)
e, err := NewExporter(ctx, &stubProtocolDriver{})
if err != nil {
t.Fatalf("failed to start an exporter: %v", err)
}

View File

@ -0,0 +1,51 @@
// 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 otlp // import "go.opentelemetry.io/otel/exporters/otlp"
import (
"context"
metricsdk "go.opentelemetry.io/otel/sdk/export/metric"
tracesdk "go.opentelemetry.io/otel/sdk/export/trace"
)
// ProtocolDriver is an interface used by OTLP exporter. It's
// responsible for connecting to and disconnecting from the collector,
// and for transforming traces and metrics into wire format and
// transmitting them to the collector.
type ProtocolDriver interface {
// Start should establish connection(s) to endpoint(s). It is
// called just once by the exporter, so the implementation
// does not need to worry about idempotence and locking.
Start(ctx context.Context) error
// Stop should close the connections. The function is called
// only once by the exporter, so the implementation does not
// need to worry about idempotence, but it may be called
// concurrently with ExportMetrics or ExportTraces, so proper
// locking is required. The function serves as a
// synchronization point - after the function returns, the
// process of closing connections is assumed to be finished.
Stop(ctx context.Context) error
// ExportMetrics should transform the passed metrics to the
// wire format and send it to the collector. May be called
// concurrently with ExportTraces, so the manager needs to
// take this into account by doing proper locking.
ExportMetrics(ctx context.Context, cps metricsdk.CheckpointSet, selector metricsdk.ExportKindSelector) error
// ExportTraces should transform the passed traces to the wire
// format and send it to the collector. May be called
// concurrently with ExportMetrics, so the manager needs to
// take this into account by doing proper locking.
ExportTraces(ctx context.Context, ss []*tracesdk.SpanSnapshot) error
}