Skip to content
This repository has been archived by the owner on Nov 7, 2022. It is now read-only.

Commit

Permalink
Set g.co/agent in Stackdriver exported via Node info (#604)
Browse files Browse the repository at this point in the history
* Set g.co/agent in Stackdriver exported via Node info

* Fixes for review comments
  • Loading branch information
draffensperger authored and Paulo Janotti committed Aug 8, 2019
1 parent b5a7ae3 commit 6de0938
Show file tree
Hide file tree
Showing 3 changed files with 295 additions and 6 deletions.
10 changes: 8 additions & 2 deletions exporter/exporterwrapper/exporterwrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ import (
spandatatranslator "github.com/census-instrumentation/opencensus-service/translator/trace/spandata"
)

// OCSpanExporter is an interface for the ExportSpan function of trace.Exporter.
// This enables passing in fake exporters in unit tests.
type OCSpanExporter interface {
ExportSpan(sd *trace.SpanData)
}

// NewExporterWrapper returns a consumer.TraceConsumer that converts OpenCensus Proto TraceData
// to OpenCensus-Go SpanData and calls into the given trace.Exporter.
//
Expand All @@ -42,7 +48,7 @@ import (
// by various vendors and contributors. Eventually the goal is to
// get those exporters converted to directly receive
// OpenCensus Proto TraceData.
func NewExporterWrapper(exporterName string, spanName string, ocExporter trace.Exporter) (exporter.TraceExporter, error) {
func NewExporterWrapper(exporterName string, spanName string, ocExporter OCSpanExporter) (exporter.TraceExporter, error) {
return exporterhelper.NewTraceExporter(
exporterName,
func(ctx context.Context, td data.TraceData) (int, error) {
Expand All @@ -57,7 +63,7 @@ func NewExporterWrapper(exporterName string, spanName string, ocExporter trace.E

// PushOcProtoSpansToOCTraceExporter pushes TraceData to the given trace.Exporter by converting the
// protos to trace.SpanData.
func PushOcProtoSpansToOCTraceExporter(ocExporter trace.Exporter, td data.TraceData) (int, error) {
func PushOcProtoSpansToOCTraceExporter(ocExporter OCSpanExporter, td data.TraceData) (int, error) {
var errs []error
var goodSpans []*tracepb.Span
for _, span := range td.Spans {
Expand Down
76 changes: 73 additions & 3 deletions exporter/stackdriverexporter/stackdriver.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,35 +17,60 @@ package stackdriverexporter
import (
"context"
"fmt"
"strings"
"sync"
"time"

"contrib.go.opencensus.io/exporter/stackdriver"
"github.com/spf13/viper"
"go.opencensus.io/trace"

commonpb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1"
metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1"
resourcepb "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1"
tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1"
"github.com/census-instrumentation/opencensus-service/consumer"
"github.com/census-instrumentation/opencensus-service/data"
"github.com/census-instrumentation/opencensus-service/exporter/exporterhelper"
"github.com/census-instrumentation/opencensus-service/exporter/exporterwrapper"
)

const agentLabel = "g.co/agent"

type stackdriverConfig struct {
ProjectID string `mapstructure:"project,omitempty"`
EnableTracing bool `mapstructure:"enable_tracing,omitempty"`
EnableMetrics bool `mapstructure:"enable_metrics,omitempty"`
MetricPrefix string `mapstructure:"metric_prefix,omitempty"`
}

// This interface and factory function type enable passing a fake Stackdriver
// exporter for a unit test.
type stackdriverExporterInterface interface {
exporterwrapper.OCSpanExporter
ExportMetricsProto(ctx context.Context, node *commonpb.Node, rsc *resourcepb.Resource, metrics []*metricspb.Metric) error
Flush()
}
type stackdriverExporterFactory = func(o stackdriver.Options) (stackdriverExporterInterface, error)

var _ stackdriverExporterInterface = (*stackdriver.Exporter)(nil)

// TODO: Add metrics support to the exporterwrapper.
type stackdriverExporter struct {
exporter *stackdriver.Exporter
exporter stackdriverExporterInterface
}

var _ consumer.MetricsConsumer = (*stackdriverExporter)(nil)

// StackdriverTraceExportersFromViper unmarshals the viper and returns an consumer.TraceConsumer targeting
// Stackdriver according to the configuration settings.
func StackdriverTraceExportersFromViper(v *viper.Viper) (tps []consumer.TraceConsumer, mps []consumer.MetricsConsumer, doneFns []func() error, err error) {
return stackdriverTraceExportersFromViperInternal(v, func(o stackdriver.Options) (stackdriverExporterInterface, error) {
return stackdriver.NewExporter(o)
})
}

func stackdriverTraceExportersFromViperInternal(v *viper.Viper, sef stackdriverExporterFactory) (tps []consumer.TraceConsumer, mps []consumer.MetricsConsumer, doneFns []func() error, err error) {
var cfg struct {
Stackdriver *stackdriverConfig `mapstructure:"stackdriver"`
}
Expand All @@ -63,7 +88,7 @@ func StackdriverTraceExportersFromViper(v *viper.Viper) (tps []consumer.TraceCon
// TODO: For each ProjectID, create a different exporter
// or at least a unique Stackdriver client per ProjectID.

sde, serr := stackdriver.NewExporter(stackdriver.Options{
sde, serr := sef(stackdriver.Options{
// If the project ID is an empty string, it will be set by default based on
// the project this is running on in GCP.
ProjectID: sc.ProjectID,
Expand All @@ -88,7 +113,13 @@ func StackdriverTraceExportersFromViper(v *viper.Viper) (tps []consumer.TraceCon
exporter: sde,
}

sdte, err := exporterwrapper.NewExporterWrapper("stackdriver_trace", "ocservice.exporter.Stackdriver.ConsumeTraceData", sde)
sdte, err := exporterhelper.NewTraceExporter(
"stackdriver_trace",
exp.pushTraceData,
exporterhelper.WithSpanName("ocservice.exporter.Stackdriver.ConsumeTraceData"),
exporterhelper.WithRecordMetrics(true),
)

if err != nil {
return nil, nil, nil, err
}
Expand All @@ -111,6 +142,38 @@ func StackdriverTraceExportersFromViper(v *viper.Viper) (tps []consumer.TraceCon
return
}

// ExportSpans is the method that translates OpenCensus-Proto Traces into AWS X-Ray spans.
// It uniquely maintains
func (sde *stackdriverExporter) pushTraceData(ctx context.Context, td data.TraceData) (int, error) {
setAgentLabelFromNode(td)
return exporterwrapper.PushOcProtoSpansToOCTraceExporter(sde.exporter, td)
}

func setAgentLabelFromNode(td data.TraceData) {
if td.Node == nil {
return
}
li := td.Node.GetLibraryInfo()
if li == nil {
return
}
agent := agentForLibraryInfo(li)
agentVal := &tracepb.AttributeValue{
Value: &tracepb.AttributeValue_StringValue{
StringValue: &tracepb.TruncatableString{Value: agent},
},
}
for _, span := range td.Spans {
if span.Attributes == nil {
span.Attributes = &tracepb.Span_Attributes{}
}
if span.Attributes.AttributeMap == nil {
span.Attributes.AttributeMap = map[string]*tracepb.AttributeValue{}
}
span.GetAttributes().GetAttributeMap()[agentLabel] = agentVal
}
}

func (sde *stackdriverExporter) ConsumeMetricsData(ctx context.Context, md data.MetricsData) error {
ctx, span := trace.StartSpan(ctx,
"opencensus.service.exporter.stackdriver.ExportMetricsData",
Expand All @@ -132,3 +195,10 @@ func (sde *stackdriverExporter) ConsumeMetricsData(ctx context.Context, md data.

return nil
}

func agentForLibraryInfo(li *commonpb.LibraryInfo) string {
langName := strings.ToLower(li.Language.String())
// The exporter must be the OpenCensus agent exporter because the spans
// have been written to the OpenCensus service.
return "opencensus-" + langName + " " + li.CoreLibraryVersion + "; ocagent-exporter " + li.ExporterVersion
}
215 changes: 214 additions & 1 deletion exporter/stackdriverexporter/stackdriverexporter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,217 @@

package stackdriverexporter

// TODO: Add tests.
import (
"context"
"reflect"
"testing"
"time"

"contrib.go.opencensus.io/exporter/stackdriver"
commonpb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1"
metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1"
resourcepb "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1"
tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1"
"github.com/golang/protobuf/ptypes/timestamp"
"github.com/spf13/viper"
"go.opencensus.io/trace"

"github.com/census-instrumentation/opencensus-service/data"
"github.com/census-instrumentation/opencensus-service/exporter/exportertest"
"github.com/census-instrumentation/opencensus-service/internal/config/viperutils"
)

type fakeStackdriverExporter struct {
spanData []*trace.SpanData
}

var _ stackdriverExporterInterface = (*fakeStackdriverExporter)(nil)

func (*fakeStackdriverExporter) ExportMetricsProto(ctx context.Context, node *commonpb.Node, rsc *resourcepb.Resource, metrics []*metricspb.Metric) error {
return nil
}

func (exp *fakeStackdriverExporter) ExportSpan(sd *trace.SpanData) {
exp.spanData = append(exp.spanData, sd)
}

func (*fakeStackdriverExporter) Flush() {
}

func fakeStackdriverNewExporter(opts stackdriver.Options) (*stackdriver.Exporter, error) {
return nil, nil
}

func TestStackriverExporter(t *testing.T) {
tests := []struct {
name string
traceData data.TraceData
want []*trace.SpanData
}{
{name: "full span with node and library info, check that agentLabel gets set",
traceData: data.TraceData{
Node: &commonpb.Node{
LibraryInfo: &commonpb.LibraryInfo{
Language: commonpb.LibraryInfo_PYTHON,
ExporterVersion: "0.4.1",
CoreLibraryVersion: "0.3.2",
},
},
Spans: []*tracepb.Span{
{
TraceId: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x80},
SpanId: []byte{0xAF, 0xAE, 0xAD, 0xAC, 0xAB, 0xAA, 0xA9, 0xA8},
Name: &tracepb.TruncatableString{Value: "DBSearch"},
StartTime: &timestamp.Timestamp{Seconds: 1550000001, Nanos: 1},
EndTime: &timestamp.Timestamp{Seconds: 1550000002, Nanos: 1},
Attributes: &tracepb.Span_Attributes{
AttributeMap: map[string]*tracepb.AttributeValue{
"cache_hit": {Value: &tracepb.AttributeValue_BoolValue{BoolValue: true}},
},
},
},
},
SourceFormat: "oc_trace",
},
want: []*trace.SpanData{
{
Name: "DBSearch",
SpanContext: trace.SpanContext{
TraceID: trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 128},
SpanID: trace.SpanID{175, 174, 173, 172, 171, 170, 169, 168},
},
StartTime: time.Unix(1550000001, 1),
EndTime: time.Unix(1550000002, 1),
// Check that the agent label is correctly formed based on the library
// info given in the Node structure above.
Attributes: map[string]interface{}{
"cache_hit": true,
agentLabel: "opencensus-python 0.3.2; ocagent-exporter 0.4.1",
},
},
},
},
{name: "no node specified, so agentLabel not set",
traceData: data.TraceData{
Spans: []*tracepb.Span{
{
Attributes: &tracepb.Span_Attributes{
AttributeMap: map[string]*tracepb.AttributeValue{
"cache_hit": {Value: &tracepb.AttributeValue_BoolValue{BoolValue: true}},
},
},
},
},
},
want: []*trace.SpanData{{Attributes: map[string]interface{}{"cache_hit": true}}},
},
{name: "no library info specified, so agentLabel not set",
traceData: data.TraceData{
Node: &commonpb.Node{
Attributes: map[string]string{"attr1": "val1"},
},
Spans: []*tracepb.Span{
{
Attributes: &tracepb.Span_Attributes{
AttributeMap: map[string]*tracepb.AttributeValue{
"cache_hit": {Value: &tracepb.AttributeValue_BoolValue{BoolValue: true}},
},
},
},
},
},
want: []*trace.SpanData{{Attributes: map[string]interface{}{"cache_hit": true}}},
},
{name: "empty library info, so agentLabel gets empty default",
traceData: data.TraceData{
Node: &commonpb.Node{
LibraryInfo: &commonpb.LibraryInfo{},
},
Spans: []*tracepb.Span{
{
Attributes: &tracepb.Span_Attributes{
AttributeMap: map[string]*tracepb.AttributeValue{
"cache_hit": {Value: &tracepb.AttributeValue_BoolValue{BoolValue: true}},
},
},
},
},
},
want: []*trace.SpanData{{Attributes: map[string]interface{}{
"cache_hit": true,
agentLabel: "opencensus-language_unspecified ; ocagent-exporter ",
}}},
},
{name: "no attributes set, still assigns agentLabel",
traceData: data.TraceData{
Node: &commonpb.Node{
LibraryInfo: &commonpb.LibraryInfo{
Language: commonpb.LibraryInfo_WEB_JS,
CoreLibraryVersion: "0.0.2",
ExporterVersion: "0.0.3",
},
},
Spans: []*tracepb.Span{{}},
},
want: []*trace.SpanData{{Attributes: map[string]interface{}{
agentLabel: "opencensus-web_js 0.0.2; ocagent-exporter 0.0.3",
}}},
},
{name: "no attributes map set, still assigns agentLabel",
traceData: data.TraceData{
Node: &commonpb.Node{
LibraryInfo: &commonpb.LibraryInfo{
Language: commonpb.LibraryInfo_WEB_JS,
CoreLibraryVersion: "0.0.2",
ExporterVersion: "0.0.3",
},
},
Spans: []*tracepb.Span{{Attributes: &tracepb.Span_Attributes{}}},
},
want: []*trace.SpanData{{Attributes: map[string]interface{}{
agentLabel: "opencensus-web_js 0.0.2; ocagent-exporter 0.0.3",
}}},
},
}

for _, tt := range tests {
v := viper.New()
configYAML := []byte(`
stackdriver:
project: 'test-project'
enable_tracing: true
enable_metrics: true
metric_prefix: 'test-metric-prefix'`)
err := viperutils.LoadYAMLBytes(v, configYAML)
exp := fakeStackdriverExporter{}
tps, mps, doneFns, err := stackdriverTraceExportersFromViperInternal(v, func(opts stackdriver.Options) (stackdriverExporterInterface, error) {
if opts.ProjectID != "test-project" {
t.Errorf("Unexpected ProjectID: %v", opts.ProjectID)
}
if opts.MetricPrefix != "test-metric-prefix" {
t.Errorf("Unexpected MetricPrefix: %v", opts.MetricPrefix)
}
return &exp, nil
})
if len(tps) != 1 {
t.Errorf("Unexpected TraceConsumer count: %v", len(tps))
}
if len(mps) != 1 {
t.Errorf("Unexpected MetricsConsumer count: %v", len(mps))
}
if len(doneFns) != 1 {
t.Errorf("Unexpected doneFns count: %v", len(doneFns))
}
if err != nil {
t.Errorf("Expected nil erorr, got %v", err)
}

tps[0].ConsumeTraceData(context.Background(), tt.traceData)

got := exp.spanData
if !reflect.DeepEqual(got, tt.want) {
gj, wj := exportertest.ToJSON(got), exportertest.ToJSON(tt.want)
t.Errorf("Test %s: Incorrect exported SpanData\nGot:\n\t%s\nWant:\n\t%s", tt.name, gj, wj)
}
}
}

0 comments on commit 6de0938

Please sign in to comment.