From 51f65a87ada66d395db64b843cee8eb840569361 Mon Sep 17 00:00:00 2001 From: Matthew Dolan Date: Tue, 10 Oct 2017 08:53:22 -0700 Subject: [PATCH] Add http collector client (#112) --- grpc_collector_client.go | 180 ++------------------- http_collector_client.go | 137 ++++++++++++++++ lightstep-tracer-common | 2 +- options.go | 9 +- proto_converter.go | 186 ++++++++++++++++++++++ grpc_logencoder.go => proto_logencoder.go | 29 ++-- thrift_collector_client.go | 4 +- tracer.go | 24 ++- 8 files changed, 382 insertions(+), 189 deletions(-) create mode 100644 http_collector_client.go create mode 100644 proto_converter.go rename grpc_logencoder.go => proto_logencoder.go (81%) diff --git a/grpc_collector_client.go b/grpc_collector_client.go index 7ce05067e..0dd286e8b 100644 --- a/grpc_collector_client.go +++ b/grpc_collector_client.go @@ -12,10 +12,7 @@ import ( "google.golang.org/grpc/credentials" // N.B.(jmacd): Do not use google.golang.org/glog in this package. - - google_protobuf "github.com/golang/protobuf/ptypes/timestamp" - cpb "github.com/lightstep/lightstep-tracer-go/collectorpb" - ot "github.com/opentracing/opentracing-go" + "github.com/lightstep/lightstep-tracer-go/collectorpb" ) const ( @@ -34,23 +31,21 @@ type grpcCollectorClient struct { attributes map[string]string reporterID uint64 - // accessToken is the access token used for explicit trace - // collection requests. + // accessToken is the access token used for explicit trace collection requests. accessToken string - - verbose bool // whether to print verbose messages - maxLogKeyLen int // see GrpcOptions.MaxLogKeyLen - maxLogValueLen int // see GrpcOptions.MaxLogValueLen maxReportingPeriod time.Duration // set by GrpcOptions.MaxReportingPeriod reconnectPeriod time.Duration // set by GrpcOptions.ReconnectPeriod reportingTimeout time.Duration // set by GrpcOptions.ReportTimeout // Remote service that will receive reports. hostPort string - grpcClient cpb.CollectorServiceClient + grpcClient collectorpb.CollectorServiceClient connTimestamp time.Time dialOptions []grpc.DialOption + // converters + converter *protoConverter + // For testing purposes only grpcConnectorFactory ConnectorFactory } @@ -61,12 +56,10 @@ func newGrpcCollectorClient(opts Options, reporterID uint64, attributes map[stri attributes: attributes, maxReportingPeriod: opts.ReportingPeriod, reportingTimeout: opts.ReportTimeout, - verbose: opts.Verbose, - maxLogKeyLen: opts.MaxLogKeyLen, - maxLogValueLen: opts.MaxLogValueLen, reporterID: reporterID, hostPort: opts.Collector.HostPort(), reconnectPeriod: time.Duration(float64(opts.ReconnectPeriod) * (1 + 0.2*rand.Float64())), + converter: newProtoConverter(opts), grpcConnectorFactory: opts.ConnFactory, } @@ -89,7 +82,7 @@ func (client *grpcCollectorClient) ConnectClient() (Connection, error) { return nil, err } - grpcClient, ok := uncheckedClient.(cpb.CollectorServiceClient) + grpcClient, ok := uncheckedClient.(collectorpb.CollectorServiceClient) if !ok { return nil, fmt.Errorf("Grpc connector factory did not provide valid client!") } @@ -103,7 +96,7 @@ func (client *grpcCollectorClient) ConnectClient() (Connection, error) { } conn = transport - client.grpcClient = cpb.NewCollectorServiceClient(transport) + client.grpcClient = collectorpb.NewCollectorServiceClient(transport) } client.connTimestamp = now return conn, nil @@ -113,158 +106,15 @@ func (client *grpcCollectorClient) ShouldReconnect() bool { return time.Now().Sub(client.connTimestamp) > client.reconnectPeriod } -func (client *grpcCollectorClient) translateTags(tags ot.Tags) []*cpb.KeyValue { - kvs := make([]*cpb.KeyValue, 0, len(tags)) - for key, tag := range tags { - kv := client.convertToKeyValue(key, tag) - kvs = append(kvs, kv) - } - return kvs -} - -func (client *grpcCollectorClient) convertToKeyValue(key string, value interface{}) *cpb.KeyValue { - kv := cpb.KeyValue{Key: key} - v := reflect.ValueOf(value) - k := v.Kind() - switch k { - case reflect.String: - kv.Value = &cpb.KeyValue_StringValue{StringValue: v.String()} - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: - kv.Value = &cpb.KeyValue_IntValue{IntValue: v.Convert(intType).Int()} - case reflect.Float32, reflect.Float64: - kv.Value = &cpb.KeyValue_DoubleValue{DoubleValue: v.Float()} - case reflect.Bool: - kv.Value = &cpb.KeyValue_BoolValue{BoolValue: v.Bool()} - default: - kv.Value = &cpb.KeyValue_StringValue{StringValue: fmt.Sprint(v)} - maybeLogInfof("value: %v, %T, is an unsupported type, and has been converted to string", client.verbose, v, v) - } - return &kv -} - -func (client *grpcCollectorClient) translateLogs(lrs []ot.LogRecord, buffer *reportBuffer) []*cpb.Log { - logs := make([]*cpb.Log, len(lrs)) - for i, lr := range lrs { - logs[i] = &cpb.Log{ - Timestamp: translateTime(lr.Timestamp), - } - marshalFields(client, logs[i], lr.Fields, buffer) - } - return logs -} - -func (client *grpcCollectorClient) translateRawSpan(rs RawSpan, buffer *reportBuffer) *cpb.Span { - s := &cpb.Span{ - SpanContext: translateSpanContext(rs.Context), - OperationName: rs.Operation, - References: translateParentSpanID(rs.ParentSpanID), - StartTimestamp: translateTime(rs.Start), - DurationMicros: translateDuration(rs.Duration), - Tags: client.translateTags(rs.Tags), - Logs: client.translateLogs(rs.Logs, buffer), - } - return s -} - -func (client *grpcCollectorClient) convertRawSpans(buffer *reportBuffer) []*cpb.Span { - spans := make([]*cpb.Span, len(buffer.rawSpans)) - for i, rs := range buffer.rawSpans { - s := client.translateRawSpan(rs, buffer) - spans[i] = s - } - return spans -} - -func (client *grpcCollectorClient) makeReportRequest(buffer *reportBuffer) *cpb.ReportRequest { - spans := client.convertRawSpans(buffer) - reporter := convertToReporter(client.attributes, client.reporterID) - - req := cpb.ReportRequest{ - Reporter: reporter, - Auth: &cpb.Auth{AccessToken: client.accessToken}, - Spans: spans, - InternalMetrics: convertToInternalMetrics(buffer), - } - return &req - -} - func (client *grpcCollectorClient) Report(ctx context.Context, buffer *reportBuffer) (collectorResponse, error) { - resp, err := client.grpcClient.Report(ctx, client.makeReportRequest(buffer)) + resp, err := client.grpcClient.Report(ctx, client.converter.toReportRequest( + client.reporterID, + client.attributes, + client.accessToken, + buffer, + )) if err != nil { return nil, err } - return resp, nil } - -func translateAttributes(atts map[string]string) []*cpb.KeyValue { - tags := make([]*cpb.KeyValue, 0, len(atts)) - for k, v := range atts { - tags = append(tags, &cpb.KeyValue{Key: k, Value: &cpb.KeyValue_StringValue{StringValue: v}}) - } - return tags -} - -func convertToReporter(atts map[string]string, id uint64) *cpb.Reporter { - return &cpb.Reporter{ - ReporterId: id, - Tags: translateAttributes(atts), - } -} - -func generateMetricsSample(b *reportBuffer) []*cpb.MetricsSample { - return []*cpb.MetricsSample{ - &cpb.MetricsSample{ - Name: spansDropped, - Value: &cpb.MetricsSample_IntValue{IntValue: b.droppedSpanCount}, - }, - &cpb.MetricsSample{ - Name: logEncoderErrors, - Value: &cpb.MetricsSample_IntValue{IntValue: b.logEncoderErrorCount}, - }, - } -} - -func convertToInternalMetrics(b *reportBuffer) *cpb.InternalMetrics { - return &cpb.InternalMetrics{ - StartTimestamp: translateTime(b.reportStart), - DurationMicros: translateDurationFromOldestYoungest(b.reportStart, b.reportEnd), - Counts: generateMetricsSample(b), - } -} - -func translateSpanContext(sc SpanContext) *cpb.SpanContext { - return &cpb.SpanContext{ - TraceId: sc.TraceID, - SpanId: sc.SpanID, - Baggage: sc.Baggage, - } -} - -func translateParentSpanID(pid uint64) []*cpb.Reference { - if pid == 0 { - return nil - } - return []*cpb.Reference{ - &cpb.Reference{ - Relationship: cpb.Reference_CHILD_OF, - SpanContext: &cpb.SpanContext{SpanId: pid}, - }, - } -} - -func translateTime(t time.Time) *google_protobuf.Timestamp { - return &google_protobuf.Timestamp{ - Seconds: t.Unix(), - Nanos: int32(t.Nanosecond()), - } -} - -func translateDuration(d time.Duration) uint64 { - return uint64(d) / 1000 -} - -func translateDurationFromOldestYoungest(ot time.Time, yt time.Time) uint64 { - return translateDuration(yt.Sub(ot)) -} diff --git a/http_collector_client.go b/http_collector_client.go new file mode 100644 index 000000000..2f69ef12f --- /dev/null +++ b/http_collector_client.go @@ -0,0 +1,137 @@ +package lightstep + +import ( + "bytes" + "github.com/golang/protobuf/proto" + "github.com/lightstep/lightstep-tracer-go/collectorpb" + "golang.org/x/net/context" + "golang.org/x/net/http2" + "io/ioutil" + "net/http" + "time" +) + +const ( + collectorHttpMethod = "POST" + collectorHttpPath = "/v1/report" + collectorHttpContentTypeHeaderValue = "application/octet-stream" + + contentTypeHeaderKey = "Content-Type" +) + +// grpcCollectorClient specifies how to send reports back to a LightStep +// collector via grpc. +type httpCollectorClient struct { + // auth and runtime information + reporterID uint64 + accessToken string // accessToken is the access token used for explicit trace collection requests. + attributes map[string]string + + reportTimeout time.Duration + + // Remote service that will receive reports. + socketAddress string + client *http.Client + + // converters + converter *protoConverter +} + +type transportCloser struct { + transport http2.Transport +} + +func (closer *transportCloser) Close() error { + closer.transport.CloseIdleConnections() + + return nil +} + +func newHttpCollectorClient(opts Options, reporterID uint64, attributes map[string]string) *httpCollectorClient { + return &httpCollectorClient{ + reporterID: reporterID, + accessToken: opts.AccessToken, + attributes: attributes, + reportTimeout: opts.ReportTimeout, + socketAddress: opts.Collector.HostPort(), + converter: newProtoConverter(opts), + } +} + +func (client *httpCollectorClient) ConnectClient() (Connection, error) { + transport := &http2.Transport{} + + client.client = &http.Client{ + Transport: transport, + Timeout: client.reportTimeout, + } + + return &transportCloser{}, nil +} + +func (client *httpCollectorClient) ShouldReconnect() bool { + // http2 will handle connection reuse under the hood + return false +} + +func (client *httpCollectorClient) Report(context context.Context, buffer *reportBuffer) (collectorResponse, error) { + httpRequest, err := client.toRequest(context, buffer) + if err != nil { + return nil, err + } + + httpResponse, err := client.client.Do(httpRequest) + if err != nil { + return nil, err + } + defer httpResponse.Body.Close() + + response, err := client.toResponse(httpResponse) + if err != nil { + return nil, err + } + + return response, nil +} + +func (client *httpCollectorClient) toRequest( + context context.Context, + buffer *reportBuffer, +) (*http.Request, error) { + protoRequest := client.converter.toReportRequest( + client.reporterID, + client.attributes, + client.accessToken, + buffer, + ) + + buf, err := proto.Marshal(protoRequest) + if err != nil { + return nil, err + } + + requestBody := bytes.NewReader(buf) + + request, err := http.NewRequest(collectorHttpMethod, client.socketAddress+collectorHttpPath, requestBody) + if err != nil { + return nil, err + } + request = request.WithContext(context) + request.Header.Set(contentTypeHeaderKey, collectorHttpContentTypeHeaderValue) + + return request, nil +} + +func (client *httpCollectorClient) toResponse(response *http.Response) (collectorResponse, error) { + body, err := ioutil.ReadAll(response.Body) + if err != nil { + return nil, err + } + + protoResponse := &collectorpb.ReportResponse{} + if err := proto.Unmarshal(body, protoResponse); err != nil { + return nil, err + } + + return protoResponse, nil +} diff --git a/lightstep-tracer-common b/lightstep-tracer-common index cfbf745fb..43cf2b6ed 160000 --- a/lightstep-tracer-common +++ b/lightstep-tracer-common @@ -1 +1 @@ -Subproject commit cfbf745fb15e7a81469ee0deafa2f01e96ffbe67 +Subproject commit 43cf2b6ed3f28c272d6277704039ee399a1e65b2 diff --git a/options.go b/options.go index a1cdeb82c..22a37d71f 100644 --- a/options.go +++ b/options.go @@ -130,11 +130,12 @@ type Options struct { // Set Verbose to true to enable more text logging. Verbose bool `yaml:"verbose"` - // DEPRECATED: set `UseThrift` to true if you do not want gRPC - UseGRPC bool `yaml:"usegrpc"` - - // Switch to + // Force the use of a specific transport protocol. + // If multiple are set to true, the following order is used to select for the first option: thrift, http, grpc. + // If none are set to true, GRPC is defaulted to. UseThrift bool `yaml:"use_thrift"` + UseHttp bool `yaml:"use_http"` + UseGRPC bool `yaml:"usegrpc"` ReconnectPeriod time.Duration `yaml:"reconnect_period"` diff --git a/proto_converter.go b/proto_converter.go new file mode 100644 index 000000000..ccdf74977 --- /dev/null +++ b/proto_converter.go @@ -0,0 +1,186 @@ +package lightstep + +import ( + "fmt" + google_protobuf "github.com/golang/protobuf/ptypes/timestamp" + cpb "github.com/lightstep/lightstep-tracer-go/collectorpb" + ot "github.com/opentracing/opentracing-go" + "reflect" + "time" +) + +type protoConverter struct { + verbose bool + maxLogKeyLen int // see GrpcOptions.MaxLogKeyLen + maxLogValueLen int // see GrpcOptions.MaxLogValueLen +} + +func newProtoConverter(options Options) *protoConverter { + return &protoConverter{ + verbose: options.Verbose, + maxLogKeyLen: options.MaxLogKeyLen, + maxLogValueLen: options.MaxLogValueLen, + } +} + +func (converter *protoConverter) toReportRequest( + reporterId uint64, + attributes map[string]string, + accessToken string, + buffer *reportBuffer, +) *cpb.ReportRequest { + return &cpb.ReportRequest{ + Reporter: converter.toReporter(reporterId, attributes), + Auth: converter.toAuth(accessToken), + Spans: converter.toSpans(buffer), + InternalMetrics: converter.toInternalMetrics(buffer), + } + +} + +func (converter *protoConverter) toReporter(reporterId uint64, attributes map[string]string) *cpb.Reporter { + return &cpb.Reporter{ + ReporterId: reporterId, + Tags: converter.toFields(attributes), + } +} + +func (converter *protoConverter) toAuth(accessToken string) *cpb.Auth { + return &cpb.Auth{ + AccessToken: accessToken, + } +} + +func (converter *protoConverter) toSpans(buffer *reportBuffer) []*cpb.Span { + spans := make([]*cpb.Span, len(buffer.rawSpans)) + for i, span := range buffer.rawSpans { + spans[i] = converter.toSpan(span, buffer) + } + return spans +} + +func (converter *protoConverter) toSpan(span RawSpan, buffer *reportBuffer) *cpb.Span { + return &cpb.Span{ + SpanContext: converter.toSpanContext(&span.Context), + OperationName: span.Operation, + References: converter.toReference(span.ParentSpanID), + StartTimestamp: converter.toTimestamp(span.Start), + DurationMicros: converter.fromDuration(span.Duration), + Tags: converter.fromTags(span.Tags), + Logs: converter.toLogs(span.Logs, buffer), + } +} + +func (converter *protoConverter) toInternalMetrics(buffer *reportBuffer) *cpb.InternalMetrics { + return &cpb.InternalMetrics{ + StartTimestamp: converter.toTimestamp(buffer.reportStart), + DurationMicros: converter.fromTimeRange(buffer.reportStart, buffer.reportEnd), + Counts: converter.toMetricsSample(buffer), + } +} + +func (converter *protoConverter) toMetricsSample(buffer *reportBuffer) []*cpb.MetricsSample { + return []*cpb.MetricsSample{ + { + Name: spansDropped, + Value: &cpb.MetricsSample_IntValue{IntValue: buffer.droppedSpanCount}, + }, + { + Name: logEncoderErrors, + Value: &cpb.MetricsSample_IntValue{IntValue: buffer.logEncoderErrorCount}, + }, + } +} + +func (converter *protoConverter) fromTags(tags ot.Tags) []*cpb.KeyValue { + fields := make([]*cpb.KeyValue, 0, len(tags)) + for key, tag := range tags { + fields = append(fields, converter.toField(key, tag)) + } + return fields +} + +func (converter *protoConverter) toField(key string, value interface{}) *cpb.KeyValue { + field := cpb.KeyValue{Key: key} + reflectedValue := reflect.ValueOf(value) + switch reflectedValue.Kind() { + case reflect.String: + field.Value = &cpb.KeyValue_StringValue{StringValue: reflectedValue.String()} + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + field.Value = &cpb.KeyValue_IntValue{IntValue: reflectedValue.Convert(intType).Int()} + case reflect.Float32, reflect.Float64: + field.Value = &cpb.KeyValue_DoubleValue{DoubleValue: reflectedValue.Float()} + case reflect.Bool: + field.Value = &cpb.KeyValue_BoolValue{BoolValue: reflectedValue.Bool()} + default: + field.Value = &cpb.KeyValue_StringValue{StringValue: fmt.Sprint(reflectedValue)} + maybeLogInfof( + "value: %v, %T, is an unsupported type, and has been converted to string", + converter.verbose, + reflectedValue, + reflectedValue, + ) + } + return &field +} + +func (converter *protoConverter) toLogs(records []ot.LogRecord, buffer *reportBuffer) []*cpb.Log { + logs := make([]*cpb.Log, len(records)) + for i, record := range records { + logs[i] = converter.toLog(record, buffer) + } + return logs +} + +func (converter *protoConverter) toLog(record ot.LogRecord, buffer *reportBuffer) *cpb.Log { + log := &cpb.Log{ + Timestamp: converter.toTimestamp(record.Timestamp), + } + marshalFields(converter, log, record.Fields, buffer) + return log +} + +func (converter *protoConverter) toFields(attributes map[string]string) []*cpb.KeyValue { + tags := make([]*cpb.KeyValue, 0, len(attributes)) + for key, value := range attributes { + tags = append(tags, converter.toField(key, value)) + } + return tags +} + +func (converter *protoConverter) toSpanContext(sc *SpanContext) *cpb.SpanContext { + return &cpb.SpanContext{ + TraceId: sc.TraceID, + SpanId: sc.SpanID, + Baggage: sc.Baggage, + } +} + +func (converter *protoConverter) toReference(parentSpanId uint64) []*cpb.Reference { + if parentSpanId == 0 { + return nil + } + return []*cpb.Reference{ + { + Relationship: cpb.Reference_CHILD_OF, + SpanContext: &cpb.SpanContext{ + SpanId: parentSpanId, + }, + }, + } +} + +func (converter *protoConverter) toTimestamp(t time.Time) *google_protobuf.Timestamp { + return &google_protobuf.Timestamp{ + Seconds: t.Unix(), + Nanos: int32(t.Nanosecond()), + } +} + +func (converter *protoConverter) fromDuration(d time.Duration) uint64 { + return uint64(d / time.Microsecond) +} + +func (converter *protoConverter) fromTimeRange(oldestTime time.Time, youngestTime time.Time) uint64 { + return converter.fromDuration(youngestTime.Sub(oldestTime)) +} diff --git a/grpc_logencoder.go b/proto_logencoder.go similarity index 81% rename from grpc_logencoder.go rename to proto_logencoder.go index 50a139f52..e99e9a817 100644 --- a/grpc_logencoder.go +++ b/proto_logencoder.go @@ -13,23 +13,26 @@ const ( // An implementation of the log.Encoder interface type grpcLogFieldEncoder struct { - recorder *grpcCollectorClient + converter *protoConverter buffer *reportBuffer currentKeyValue *cpb.KeyValue } func marshalFields( - recorder *grpcCollectorClient, + converter *protoConverter, protoLog *cpb.Log, fields []log.Field, buffer *reportBuffer, ) { - lfe := grpcLogFieldEncoder{recorder, buffer, nil} + logFieldEncoder := grpcLogFieldEncoder{ + converter: converter, + buffer: buffer, + } protoLog.Fields = make([]*cpb.KeyValue, len(fields)) - for i, f := range fields { - lfe.currentKeyValue = &cpb.KeyValue{} - f.Marshal(&lfe) - protoLog.Fields[i] = lfe.currentKeyValue + for i, field := range fields { + logFieldEncoder.currentKeyValue = &cpb.KeyValue{} + field.Marshal(&logFieldEncoder) + protoLog.Fields[i] = logFieldEncoder.currentKeyValue } } @@ -85,20 +88,20 @@ func (lfe *grpcLogFieldEncoder) EmitLazyLogger(value log.LazyLogger) { } func (lfe *grpcLogFieldEncoder) emitSafeKey(key string) { - if len(key) > lfe.recorder.maxLogKeyLen { - key = key[:(lfe.recorder.maxLogKeyLen-1)] + ellipsis + if len(key) > lfe.converter.maxLogKeyLen { + key = key[:(lfe.converter.maxLogKeyLen-1)] + ellipsis } lfe.currentKeyValue.Key = key } func (lfe *grpcLogFieldEncoder) emitSafeString(str string) { - if len(str) > lfe.recorder.maxLogValueLen { - str = str[:(lfe.recorder.maxLogValueLen-1)] + ellipsis + if len(str) > lfe.converter.maxLogValueLen { + str = str[:(lfe.converter.maxLogValueLen-1)] + ellipsis } lfe.currentKeyValue.Value = &cpb.KeyValue_StringValue{str} } func (lfe *grpcLogFieldEncoder) emitSafeJSON(json string) { - if len(json) > lfe.recorder.maxLogValueLen { - str := json[:(lfe.recorder.maxLogValueLen-1)] + ellipsis + if len(json) > lfe.converter.maxLogValueLen { + str := json[:(lfe.converter.maxLogValueLen-1)] + ellipsis lfe.currentKeyValue.Value = &cpb.KeyValue_StringValue{str} return } diff --git a/thrift_collector_client.go b/thrift_collector_client.go index 79cec5714..c713c5abc 100644 --- a/thrift_collector_client.go +++ b/thrift_collector_client.go @@ -97,7 +97,9 @@ func (client *thriftCollectorClient) ConnectClient() (Connection, error) { conn = transport client.thriftClient = lightstep_thrift.NewReportingServiceClientFactory( - transport, thrift.NewTBinaryProtocolFactoryDefault()) + transport, + thrift.NewTBinaryProtocolFactoryDefault(), + ) } return conn, nil } diff --git a/tracer.go b/tracer.go index ca194359c..eb25fffeb 100644 --- a/tracer.go +++ b/tracer.go @@ -121,11 +121,7 @@ func NewTracer(opts Options) Tracer { impl.buffer.setCurrent(now) - if opts.UseThrift { - impl.client = newThriftCollectorClient(opts, impl.reporterID, attributes) - } else { - impl.client = newGrpcCollectorClient(opts, impl.reporterID, attributes) - } + impl.client = newClient(opts, impl.reporterID, attributes) conn, err := impl.client.ConnectClient() if err != nil { @@ -147,6 +143,24 @@ func NewTracer(opts Options) Tracer { return impl } + +func newClient(opts Options, reporterId uint64, attributes map[string]string) collectorClient { + if opts.UseThrift { + return newThriftCollectorClient(opts, reporterId, attributes) + } + + if opts.UseHttp { + return newHttpCollectorClient(opts, reporterId, attributes) + } + + if opts.UseGRPC { + return newGrpcCollectorClient(opts, reporterId, attributes) + } + + // No transport specified, defaulting to GRPC + return newGrpcCollectorClient(opts, reporterId, attributes) +} + func (tracer *tracerImpl) Options() Options { return tracer.opts }