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

Commit 6de0938

Browse files
draffenspergerPaulo Janotti
authored andcommitted
Set g.co/agent in Stackdriver exported via Node info (#604)
* Set g.co/agent in Stackdriver exported via Node info * Fixes for review comments
1 parent b5a7ae3 commit 6de0938

File tree

3 files changed

+295
-6
lines changed

3 files changed

+295
-6
lines changed

exporter/exporterwrapper/exporterwrapper.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ import (
3434
spandatatranslator "github.com/census-instrumentation/opencensus-service/translator/trace/spandata"
3535
)
3636

37+
// OCSpanExporter is an interface for the ExportSpan function of trace.Exporter.
38+
// This enables passing in fake exporters in unit tests.
39+
type OCSpanExporter interface {
40+
ExportSpan(sd *trace.SpanData)
41+
}
42+
3743
// NewExporterWrapper returns a consumer.TraceConsumer that converts OpenCensus Proto TraceData
3844
// to OpenCensus-Go SpanData and calls into the given trace.Exporter.
3945
//
@@ -42,7 +48,7 @@ import (
4248
// by various vendors and contributors. Eventually the goal is to
4349
// get those exporters converted to directly receive
4450
// OpenCensus Proto TraceData.
45-
func NewExporterWrapper(exporterName string, spanName string, ocExporter trace.Exporter) (exporter.TraceExporter, error) {
51+
func NewExporterWrapper(exporterName string, spanName string, ocExporter OCSpanExporter) (exporter.TraceExporter, error) {
4652
return exporterhelper.NewTraceExporter(
4753
exporterName,
4854
func(ctx context.Context, td data.TraceData) (int, error) {
@@ -57,7 +63,7 @@ func NewExporterWrapper(exporterName string, spanName string, ocExporter trace.E
5763

5864
// PushOcProtoSpansToOCTraceExporter pushes TraceData to the given trace.Exporter by converting the
5965
// protos to trace.SpanData.
60-
func PushOcProtoSpansToOCTraceExporter(ocExporter trace.Exporter, td data.TraceData) (int, error) {
66+
func PushOcProtoSpansToOCTraceExporter(ocExporter OCSpanExporter, td data.TraceData) (int, error) {
6167
var errs []error
6268
var goodSpans []*tracepb.Span
6369
for _, span := range td.Spans {

exporter/stackdriverexporter/stackdriver.go

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,35 +17,60 @@ package stackdriverexporter
1717
import (
1818
"context"
1919
"fmt"
20+
"strings"
2021
"sync"
2122
"time"
2223

2324
"contrib.go.opencensus.io/exporter/stackdriver"
2425
"github.com/spf13/viper"
2526
"go.opencensus.io/trace"
2627

28+
commonpb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1"
29+
metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1"
30+
resourcepb "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1"
31+
tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1"
2732
"github.com/census-instrumentation/opencensus-service/consumer"
2833
"github.com/census-instrumentation/opencensus-service/data"
34+
"github.com/census-instrumentation/opencensus-service/exporter/exporterhelper"
2935
"github.com/census-instrumentation/opencensus-service/exporter/exporterwrapper"
3036
)
3137

38+
const agentLabel = "g.co/agent"
39+
3240
type stackdriverConfig struct {
3341
ProjectID string `mapstructure:"project,omitempty"`
3442
EnableTracing bool `mapstructure:"enable_tracing,omitempty"`
3543
EnableMetrics bool `mapstructure:"enable_metrics,omitempty"`
3644
MetricPrefix string `mapstructure:"metric_prefix,omitempty"`
3745
}
3846

47+
// This interface and factory function type enable passing a fake Stackdriver
48+
// exporter for a unit test.
49+
type stackdriverExporterInterface interface {
50+
exporterwrapper.OCSpanExporter
51+
ExportMetricsProto(ctx context.Context, node *commonpb.Node, rsc *resourcepb.Resource, metrics []*metricspb.Metric) error
52+
Flush()
53+
}
54+
type stackdriverExporterFactory = func(o stackdriver.Options) (stackdriverExporterInterface, error)
55+
56+
var _ stackdriverExporterInterface = (*stackdriver.Exporter)(nil)
57+
3958
// TODO: Add metrics support to the exporterwrapper.
4059
type stackdriverExporter struct {
41-
exporter *stackdriver.Exporter
60+
exporter stackdriverExporterInterface
4261
}
4362

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

4665
// StackdriverTraceExportersFromViper unmarshals the viper and returns an consumer.TraceConsumer targeting
4766
// Stackdriver according to the configuration settings.
4867
func StackdriverTraceExportersFromViper(v *viper.Viper) (tps []consumer.TraceConsumer, mps []consumer.MetricsConsumer, doneFns []func() error, err error) {
68+
return stackdriverTraceExportersFromViperInternal(v, func(o stackdriver.Options) (stackdriverExporterInterface, error) {
69+
return stackdriver.NewExporter(o)
70+
})
71+
}
72+
73+
func stackdriverTraceExportersFromViperInternal(v *viper.Viper, sef stackdriverExporterFactory) (tps []consumer.TraceConsumer, mps []consumer.MetricsConsumer, doneFns []func() error, err error) {
4974
var cfg struct {
5075
Stackdriver *stackdriverConfig `mapstructure:"stackdriver"`
5176
}
@@ -63,7 +88,7 @@ func StackdriverTraceExportersFromViper(v *viper.Viper) (tps []consumer.TraceCon
6388
// TODO: For each ProjectID, create a different exporter
6489
// or at least a unique Stackdriver client per ProjectID.
6590

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

91-
sdte, err := exporterwrapper.NewExporterWrapper("stackdriver_trace", "ocservice.exporter.Stackdriver.ConsumeTraceData", sde)
116+
sdte, err := exporterhelper.NewTraceExporter(
117+
"stackdriver_trace",
118+
exp.pushTraceData,
119+
exporterhelper.WithSpanName("ocservice.exporter.Stackdriver.ConsumeTraceData"),
120+
exporterhelper.WithRecordMetrics(true),
121+
)
122+
92123
if err != nil {
93124
return nil, nil, nil, err
94125
}
@@ -111,6 +142,38 @@ func StackdriverTraceExportersFromViper(v *viper.Viper) (tps []consumer.TraceCon
111142
return
112143
}
113144

145+
// ExportSpans is the method that translates OpenCensus-Proto Traces into AWS X-Ray spans.
146+
// It uniquely maintains
147+
func (sde *stackdriverExporter) pushTraceData(ctx context.Context, td data.TraceData) (int, error) {
148+
setAgentLabelFromNode(td)
149+
return exporterwrapper.PushOcProtoSpansToOCTraceExporter(sde.exporter, td)
150+
}
151+
152+
func setAgentLabelFromNode(td data.TraceData) {
153+
if td.Node == nil {
154+
return
155+
}
156+
li := td.Node.GetLibraryInfo()
157+
if li == nil {
158+
return
159+
}
160+
agent := agentForLibraryInfo(li)
161+
agentVal := &tracepb.AttributeValue{
162+
Value: &tracepb.AttributeValue_StringValue{
163+
StringValue: &tracepb.TruncatableString{Value: agent},
164+
},
165+
}
166+
for _, span := range td.Spans {
167+
if span.Attributes == nil {
168+
span.Attributes = &tracepb.Span_Attributes{}
169+
}
170+
if span.Attributes.AttributeMap == nil {
171+
span.Attributes.AttributeMap = map[string]*tracepb.AttributeValue{}
172+
}
173+
span.GetAttributes().GetAttributeMap()[agentLabel] = agentVal
174+
}
175+
}
176+
114177
func (sde *stackdriverExporter) ConsumeMetricsData(ctx context.Context, md data.MetricsData) error {
115178
ctx, span := trace.StartSpan(ctx,
116179
"opencensus.service.exporter.stackdriver.ExportMetricsData",
@@ -132,3 +195,10 @@ func (sde *stackdriverExporter) ConsumeMetricsData(ctx context.Context, md data.
132195

133196
return nil
134197
}
198+
199+
func agentForLibraryInfo(li *commonpb.LibraryInfo) string {
200+
langName := strings.ToLower(li.Language.String())
201+
// The exporter must be the OpenCensus agent exporter because the spans
202+
// have been written to the OpenCensus service.
203+
return "opencensus-" + langName + " " + li.CoreLibraryVersion + "; ocagent-exporter " + li.ExporterVersion
204+
}

exporter/stackdriverexporter/stackdriverexporter_test.go

Lines changed: 214 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,217 @@
1414

1515
package stackdriverexporter
1616

17-
// TODO: Add tests.
17+
import (
18+
"context"
19+
"reflect"
20+
"testing"
21+
"time"
22+
23+
"contrib.go.opencensus.io/exporter/stackdriver"
24+
commonpb "github.com/census-instrumentation/opencensus-proto/gen-go/agent/common/v1"
25+
metricspb "github.com/census-instrumentation/opencensus-proto/gen-go/metrics/v1"
26+
resourcepb "github.com/census-instrumentation/opencensus-proto/gen-go/resource/v1"
27+
tracepb "github.com/census-instrumentation/opencensus-proto/gen-go/trace/v1"
28+
"github.com/golang/protobuf/ptypes/timestamp"
29+
"github.com/spf13/viper"
30+
"go.opencensus.io/trace"
31+
32+
"github.com/census-instrumentation/opencensus-service/data"
33+
"github.com/census-instrumentation/opencensus-service/exporter/exportertest"
34+
"github.com/census-instrumentation/opencensus-service/internal/config/viperutils"
35+
)
36+
37+
type fakeStackdriverExporter struct {
38+
spanData []*trace.SpanData
39+
}
40+
41+
var _ stackdriverExporterInterface = (*fakeStackdriverExporter)(nil)
42+
43+
func (*fakeStackdriverExporter) ExportMetricsProto(ctx context.Context, node *commonpb.Node, rsc *resourcepb.Resource, metrics []*metricspb.Metric) error {
44+
return nil
45+
}
46+
47+
func (exp *fakeStackdriverExporter) ExportSpan(sd *trace.SpanData) {
48+
exp.spanData = append(exp.spanData, sd)
49+
}
50+
51+
func (*fakeStackdriverExporter) Flush() {
52+
}
53+
54+
func fakeStackdriverNewExporter(opts stackdriver.Options) (*stackdriver.Exporter, error) {
55+
return nil, nil
56+
}
57+
58+
func TestStackriverExporter(t *testing.T) {
59+
tests := []struct {
60+
name string
61+
traceData data.TraceData
62+
want []*trace.SpanData
63+
}{
64+
{name: "full span with node and library info, check that agentLabel gets set",
65+
traceData: data.TraceData{
66+
Node: &commonpb.Node{
67+
LibraryInfo: &commonpb.LibraryInfo{
68+
Language: commonpb.LibraryInfo_PYTHON,
69+
ExporterVersion: "0.4.1",
70+
CoreLibraryVersion: "0.3.2",
71+
},
72+
},
73+
Spans: []*tracepb.Span{
74+
{
75+
TraceId: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x80},
76+
SpanId: []byte{0xAF, 0xAE, 0xAD, 0xAC, 0xAB, 0xAA, 0xA9, 0xA8},
77+
Name: &tracepb.TruncatableString{Value: "DBSearch"},
78+
StartTime: &timestamp.Timestamp{Seconds: 1550000001, Nanos: 1},
79+
EndTime: &timestamp.Timestamp{Seconds: 1550000002, Nanos: 1},
80+
Attributes: &tracepb.Span_Attributes{
81+
AttributeMap: map[string]*tracepb.AttributeValue{
82+
"cache_hit": {Value: &tracepb.AttributeValue_BoolValue{BoolValue: true}},
83+
},
84+
},
85+
},
86+
},
87+
SourceFormat: "oc_trace",
88+
},
89+
want: []*trace.SpanData{
90+
{
91+
Name: "DBSearch",
92+
SpanContext: trace.SpanContext{
93+
TraceID: trace.TraceID{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 128},
94+
SpanID: trace.SpanID{175, 174, 173, 172, 171, 170, 169, 168},
95+
},
96+
StartTime: time.Unix(1550000001, 1),
97+
EndTime: time.Unix(1550000002, 1),
98+
// Check that the agent label is correctly formed based on the library
99+
// info given in the Node structure above.
100+
Attributes: map[string]interface{}{
101+
"cache_hit": true,
102+
agentLabel: "opencensus-python 0.3.2; ocagent-exporter 0.4.1",
103+
},
104+
},
105+
},
106+
},
107+
{name: "no node specified, so agentLabel not set",
108+
traceData: data.TraceData{
109+
Spans: []*tracepb.Span{
110+
{
111+
Attributes: &tracepb.Span_Attributes{
112+
AttributeMap: map[string]*tracepb.AttributeValue{
113+
"cache_hit": {Value: &tracepb.AttributeValue_BoolValue{BoolValue: true}},
114+
},
115+
},
116+
},
117+
},
118+
},
119+
want: []*trace.SpanData{{Attributes: map[string]interface{}{"cache_hit": true}}},
120+
},
121+
{name: "no library info specified, so agentLabel not set",
122+
traceData: data.TraceData{
123+
Node: &commonpb.Node{
124+
Attributes: map[string]string{"attr1": "val1"},
125+
},
126+
Spans: []*tracepb.Span{
127+
{
128+
Attributes: &tracepb.Span_Attributes{
129+
AttributeMap: map[string]*tracepb.AttributeValue{
130+
"cache_hit": {Value: &tracepb.AttributeValue_BoolValue{BoolValue: true}},
131+
},
132+
},
133+
},
134+
},
135+
},
136+
want: []*trace.SpanData{{Attributes: map[string]interface{}{"cache_hit": true}}},
137+
},
138+
{name: "empty library info, so agentLabel gets empty default",
139+
traceData: data.TraceData{
140+
Node: &commonpb.Node{
141+
LibraryInfo: &commonpb.LibraryInfo{},
142+
},
143+
Spans: []*tracepb.Span{
144+
{
145+
Attributes: &tracepb.Span_Attributes{
146+
AttributeMap: map[string]*tracepb.AttributeValue{
147+
"cache_hit": {Value: &tracepb.AttributeValue_BoolValue{BoolValue: true}},
148+
},
149+
},
150+
},
151+
},
152+
},
153+
want: []*trace.SpanData{{Attributes: map[string]interface{}{
154+
"cache_hit": true,
155+
agentLabel: "opencensus-language_unspecified ; ocagent-exporter ",
156+
}}},
157+
},
158+
{name: "no attributes set, still assigns agentLabel",
159+
traceData: data.TraceData{
160+
Node: &commonpb.Node{
161+
LibraryInfo: &commonpb.LibraryInfo{
162+
Language: commonpb.LibraryInfo_WEB_JS,
163+
CoreLibraryVersion: "0.0.2",
164+
ExporterVersion: "0.0.3",
165+
},
166+
},
167+
Spans: []*tracepb.Span{{}},
168+
},
169+
want: []*trace.SpanData{{Attributes: map[string]interface{}{
170+
agentLabel: "opencensus-web_js 0.0.2; ocagent-exporter 0.0.3",
171+
}}},
172+
},
173+
{name: "no attributes map set, still assigns agentLabel",
174+
traceData: data.TraceData{
175+
Node: &commonpb.Node{
176+
LibraryInfo: &commonpb.LibraryInfo{
177+
Language: commonpb.LibraryInfo_WEB_JS,
178+
CoreLibraryVersion: "0.0.2",
179+
ExporterVersion: "0.0.3",
180+
},
181+
},
182+
Spans: []*tracepb.Span{{Attributes: &tracepb.Span_Attributes{}}},
183+
},
184+
want: []*trace.SpanData{{Attributes: map[string]interface{}{
185+
agentLabel: "opencensus-web_js 0.0.2; ocagent-exporter 0.0.3",
186+
}}},
187+
},
188+
}
189+
190+
for _, tt := range tests {
191+
v := viper.New()
192+
configYAML := []byte(`
193+
stackdriver:
194+
project: 'test-project'
195+
enable_tracing: true
196+
enable_metrics: true
197+
metric_prefix: 'test-metric-prefix'`)
198+
err := viperutils.LoadYAMLBytes(v, configYAML)
199+
exp := fakeStackdriverExporter{}
200+
tps, mps, doneFns, err := stackdriverTraceExportersFromViperInternal(v, func(opts stackdriver.Options) (stackdriverExporterInterface, error) {
201+
if opts.ProjectID != "test-project" {
202+
t.Errorf("Unexpected ProjectID: %v", opts.ProjectID)
203+
}
204+
if opts.MetricPrefix != "test-metric-prefix" {
205+
t.Errorf("Unexpected MetricPrefix: %v", opts.MetricPrefix)
206+
}
207+
return &exp, nil
208+
})
209+
if len(tps) != 1 {
210+
t.Errorf("Unexpected TraceConsumer count: %v", len(tps))
211+
}
212+
if len(mps) != 1 {
213+
t.Errorf("Unexpected MetricsConsumer count: %v", len(mps))
214+
}
215+
if len(doneFns) != 1 {
216+
t.Errorf("Unexpected doneFns count: %v", len(doneFns))
217+
}
218+
if err != nil {
219+
t.Errorf("Expected nil erorr, got %v", err)
220+
}
221+
222+
tps[0].ConsumeTraceData(context.Background(), tt.traceData)
223+
224+
got := exp.spanData
225+
if !reflect.DeepEqual(got, tt.want) {
226+
gj, wj := exportertest.ToJSON(got), exportertest.ToJSON(tt.want)
227+
t.Errorf("Test %s: Incorrect exported SpanData\nGot:\n\t%s\nWant:\n\t%s", tt.name, gj, wj)
228+
}
229+
}
230+
}

0 commit comments

Comments
 (0)