Skip to content
This repository was archived by the owner on Oct 3, 2023. It is now read-only.

Commit 55a5545

Browse files
authored
Exporter/Stackdriver: Use Trace SDKs (v2 API) (#338)
* OpenCensus Stackdriver Trace Exporter is updated to use Stackdriver Trace V2 APIs. * fix review comments
1 parent 515789e commit 55a5545

File tree

7 files changed

+746
-149
lines changed

7 files changed

+746
-149
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
1212
- Add ```opencensus-resource-util``` to auto detect AWS, GCE and Kubernetes(K8S) monitored resource, based on the environment where the application is running.
1313
- Add optional `uncompressedSize` and `compressedSize` fields to `MessageEvent` interface.
1414
- Add a ```setStatus``` method in the Span.
15+
- OpenCensus Stackdriver Trace Exporter is updated to use Stackdriver Trace V2 APIs.
1516

1617
**This release has multiple breaking changes. Please test your code accordingly after upgrading.**
1718

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
/**
2+
* Copyright 2019, OpenCensus Authors
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import * as coreTypes from '@opencensus/core';
18+
import * as types from './types';
19+
20+
const AGENT_LABEL_KEY = 'g.co/agent';
21+
const AGENT_LABEL_VALUE_STRING = `opencensus-node [${coreTypes.version}]`;
22+
const AGENT_LABEL_VALUE = createAttributeValue(AGENT_LABEL_VALUE_STRING);
23+
24+
/**
25+
* Creates StackDriver Links from OpenCensus Link.
26+
* @param links coreTypes.Link[]
27+
* @param droppedLinksCount number
28+
* @returns types.Links
29+
*/
30+
export function createLinks(
31+
links: coreTypes.Link[], droppedLinksCount: number): types.Links {
32+
return {link: links.map((link) => createLink(link)), droppedLinksCount};
33+
}
34+
35+
/**
36+
* Creates StackDriver Attributes from OpenCensus Attributes.
37+
* @param attributes coreTypes.Attributes
38+
* @param resourceLabels Record<string, types.AttributeValue>
39+
* @param droppedAttributesCount number
40+
* @returns types.Attributes
41+
*/
42+
export function createAttributes(
43+
attributes: coreTypes.Attributes,
44+
resourceLabels: Record<string, types.AttributeValue>,
45+
droppedAttributesCount: number): types.Attributes {
46+
const attributesBuilder =
47+
createAttributesBuilder(attributes, droppedAttributesCount);
48+
attributesBuilder.attributeMap[AGENT_LABEL_KEY] = AGENT_LABEL_VALUE;
49+
attributesBuilder.attributeMap =
50+
Object.assign({}, attributesBuilder.attributeMap, resourceLabels);
51+
return attributesBuilder;
52+
}
53+
54+
/**
55+
* Creates StackDriver TimeEvents from OpenCensus Annotation and MessageEvent.
56+
* @param annotationTimedEvents coreTypes.Annotation[]
57+
* @param messageEventTimedEvents coreTypes.MessageEvent[]
58+
* @param droppedAnnotationsCount number
59+
* @param droppedMessageEventsCount number
60+
* @returns types.TimeEvents
61+
*/
62+
export function createTimeEvents(
63+
annotationTimedEvents: coreTypes.Annotation[],
64+
messageEventTimedEvents: coreTypes.MessageEvent[],
65+
droppedAnnotationsCount: number,
66+
droppedMessageEventsCount: number): types.TimeEvents {
67+
let timeEvents: types.TimeEvent[] = [];
68+
if (annotationTimedEvents) {
69+
timeEvents = annotationTimedEvents.map(
70+
(annotation) => ({
71+
time: new Date(annotation.timestamp).toISOString(),
72+
annotation: {
73+
description: stringToTruncatableString(annotation.description),
74+
attributes: createAttributesBuilder(annotation.attributes, 0)
75+
}
76+
}));
77+
}
78+
if (messageEventTimedEvents) {
79+
timeEvents.push(...messageEventTimedEvents.map(
80+
(messageEvent) => ({
81+
time: new Date(messageEvent.timestamp).toISOString(),
82+
messageEvent: {
83+
id: messageEvent.id,
84+
type: createMessageEventType(messageEvent.type)
85+
}
86+
})));
87+
}
88+
return {
89+
timeEvent: timeEvents,
90+
droppedAnnotationsCount,
91+
droppedMessageEventsCount
92+
};
93+
}
94+
95+
export function stringToTruncatableString(value: string):
96+
types.TruncatableString {
97+
return {value};
98+
}
99+
100+
export async function getResourceLabels(
101+
monitoredResource: Promise<types.MonitoredResource>) {
102+
const resource = await monitoredResource;
103+
const resourceLabels: Record<string, types.AttributeValue> = {};
104+
if (resource.type === 'global') {
105+
return resourceLabels;
106+
}
107+
for (const key of Object.keys(resource.labels)) {
108+
const resourceLabel = `g.co/r/${resource.type}/${key}`;
109+
resourceLabels[resourceLabel] = createAttributeValue(resource.labels[key]);
110+
}
111+
return resourceLabels;
112+
}
113+
114+
function createAttributesBuilder(
115+
attributes: coreTypes.Attributes,
116+
droppedAttributesCount: number): types.Attributes {
117+
const attributeMap: Record<string, types.AttributeValue> = {};
118+
for (const key of Object.keys(attributes)) {
119+
attributeMap[key] = createAttributeValue(attributes[key]);
120+
}
121+
return {attributeMap, droppedAttributesCount};
122+
}
123+
124+
function createLink(link: coreTypes.Link): types.Link {
125+
const traceId = link.traceId;
126+
const spanId = link.spanId;
127+
const type = createLinkType(link.type);
128+
const attributes = createAttributesBuilder(link.attributes, 0);
129+
return {traceId, spanId, type, attributes};
130+
}
131+
132+
function createAttributeValue(value: string|number|
133+
boolean): types.AttributeValue {
134+
switch (typeof value) {
135+
case 'number':
136+
// TODO: Consider to change to doubleValue when available in V2 API.
137+
return {intValue: String(value)};
138+
case 'boolean':
139+
return {boolValue: value as boolean};
140+
case 'string':
141+
return {stringValue: stringToTruncatableString(value)};
142+
default:
143+
throw new Error(`Unsupported type : ${typeof value}`);
144+
}
145+
}
146+
147+
function createMessageEventType(type: coreTypes.MessageEventType) {
148+
switch (type) {
149+
case coreTypes.MessageEventType.SENT: {
150+
return types.Type.SENT;
151+
}
152+
case coreTypes.MessageEventType.RECEIVED: {
153+
return types.Type.RECEIVED;
154+
}
155+
default: { return types.Type.TYPE_UNSPECIFIED; }
156+
}
157+
}
158+
159+
function createLinkType(type: coreTypes.LinkType) {
160+
switch (type) {
161+
case coreTypes.LinkType.CHILD_LINKED_SPAN: {
162+
return types.LinkType.CHILD_LINKED_SPAN;
163+
}
164+
case coreTypes.LinkType.PARENT_LINKED_SPAN: {
165+
return types.LinkType.PARENT_LINKED_SPAN;
166+
}
167+
default: { return types.LinkType.UNSPECIFIED; }
168+
}
169+
}

packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace.ts

Lines changed: 72 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,32 @@
1414
* limitations under the License.
1515
*/
1616

17-
import {Exporter, ExporterBuffer, RootSpan, Span, SpanContext} from '@opencensus/core';
17+
import {Exporter, ExporterBuffer, RootSpan, Span as OCSpan, SpanContext} from '@opencensus/core';
1818
import {logger, Logger} from '@opencensus/core';
1919
import {auth, JWT} from 'google-auth-library';
2020
import {google} from 'googleapis';
21-
// TODO change to use import when types for hex2dec will be available
22-
const {hexToDec}: {[key: string]: (input: string) => string} =
23-
require('hex2dec');
24-
import {StackdriverExporterOptions, TracesWithCredentials, TranslatedSpan, TranslatedTrace} from './types';
21+
22+
import {getDefaultResource} from './common-utils';
23+
import {createAttributes, createLinks, createTimeEvents, getResourceLabels, stringToTruncatableString} from './stackdriver-cloudtrace-utils';
24+
import {AttributeValue, Span, SpansWithCredentials, StackdriverExporterOptions} from './types';
2525

2626
google.options({headers: {'x-opencensus-outgoing-request': 0x1}});
27-
const cloudTrace = google.cloudtrace('v1');
27+
const cloudTrace = google.cloudtrace('v2');
2828

2929
/** Format and sends span information to Stackdriver */
3030
export class StackdriverTraceExporter implements Exporter {
3131
projectId: string;
3232
exporterBuffer: ExporterBuffer;
3333
logger: Logger;
3434
failBuffer: SpanContext[] = [];
35+
private RESOURCE_LABELS: Promise<Record<string, AttributeValue>>;
3536

3637
constructor(options: StackdriverExporterOptions) {
3738
this.projectId = options.projectId;
3839
this.logger = options.logger || logger.logger();
3940
this.exporterBuffer = new ExporterBuffer(this, options);
41+
this.RESOURCE_LABELS =
42+
getResourceLabels(getDefaultResource(this.projectId));
4043
}
4144

4245
/**
@@ -54,13 +57,12 @@ export class StackdriverTraceExporter implements Exporter {
5457
* Publishes a list of root spans to Stackdriver.
5558
* @param rootSpans
5659
*/
57-
publish(rootSpans: RootSpan[]) {
58-
const stackdriverTraces =
59-
rootSpans.map(trace => this.translateTrace(trace));
60+
async publish(rootSpans: RootSpan[]) {
61+
const spanList = await this.translateSpan(rootSpans);
6062

61-
return this.authorize(stackdriverTraces)
62-
.then((traces: TracesWithCredentials) => {
63-
return this.sendTrace(traces);
63+
return this.authorize(spanList)
64+
.then((spans: SpansWithCredentials) => {
65+
return this.batchWriteSpans(spans);
6466
})
6567
.catch(err => {
6668
for (const root of rootSpans) {
@@ -70,51 +72,70 @@ export class StackdriverTraceExporter implements Exporter {
7072
});
7173
}
7274

73-
/**
74-
* Translates root span data to Stackdriver's trace format.
75-
* @param root
76-
*/
77-
private translateTrace(root: RootSpan): TranslatedTrace {
78-
const spanList = root.spans.map((span: Span) => this.translateSpan(span));
79-
spanList.push(this.translateSpan(root));
80-
81-
return {projectId: this.projectId, traceId: root.traceId, spans: spanList};
75+
async translateSpan(rootSpans: RootSpan[]) {
76+
const resourceLabel = await this.RESOURCE_LABELS;
77+
const spanList: Span[] = [];
78+
rootSpans.forEach(rootSpan => {
79+
// RootSpan data
80+
spanList.push(this.createSpan(rootSpan, resourceLabel));
81+
rootSpan.spans.forEach(span => {
82+
// Builds spans data
83+
spanList.push(this.createSpan(span, resourceLabel));
84+
});
85+
});
86+
return spanList;
8287
}
8388

84-
/**
85-
* Translates span data to Stackdriver's span format.
86-
* @param span
87-
*/
88-
private translateSpan(span: Span): TranslatedSpan {
89-
return {
90-
name: span.name,
91-
kind: 'SPAN_KIND_UNSPECIFIED',
92-
spanId: hexToDec(span.id),
93-
startTime: span.startTime,
94-
endTime: span.endTime,
95-
labels: Object.keys(span.attributes)
96-
.reduce(
97-
(acc, k) => {
98-
acc[k] = String(span.attributes[k]);
99-
return acc;
100-
},
101-
{} as Record<string, string>)
89+
private createSpan(
90+
span: OCSpan, resourceLabels: Record<string, AttributeValue>): Span {
91+
const spanName =
92+
`projects/${this.projectId}/traces/${span.traceId}/spans/${span.id}`;
93+
94+
const spanBuilder: Span = {
95+
name: spanName,
96+
spanId: span.id,
97+
displayName: stringToTruncatableString(span.name),
98+
startTime: span.startTime.toISOString(),
99+
endTime: span.endTime.toISOString(),
100+
attributes: createAttributes(
101+
span.attributes, resourceLabels, span.droppedAttributesCount),
102+
timeEvents: createTimeEvents(
103+
span.annotations, span.messageEvents, span.droppedAnnotationsCount,
104+
span.droppedMessageEventsCount),
105+
links: createLinks(span.links, span.droppedLinksCount),
106+
status: {code: span.status.code},
107+
sameProcessAsParentSpan: !span.remoteParent,
108+
childSpanCount: null, // TODO: Consider to add count after pull/332
109+
stackTrace: null, // Unsupported by nodejs
102110
};
111+
if (span.parentSpanId) {
112+
spanBuilder.parentSpanId = span.parentSpanId;
113+
}
114+
if (span.status.message) {
115+
spanBuilder.status.message = span.status.message;
116+
}
117+
118+
return spanBuilder;
103119
}
104120

105121
/**
106-
* Sends traces in the Stackdriver format to the service.
107-
* @param traces
122+
* Sends new spans to new or existing traces in the Stackdriver format to the
123+
* service.
124+
* @param spans
108125
*/
109-
private sendTrace(traces: TracesWithCredentials) {
126+
private batchWriteSpans(spans: SpansWithCredentials) {
110127
return new Promise((resolve, reject) => {
111-
cloudTrace.projects.patchTraces(traces, (err: Error) => {
128+
// TODO: Consider to use gRPC call (BatchWriteSpansRequest) for sending
129+
// data to backend :
130+
// https://cloud.google.com/trace/docs/reference/v2/rpc/google.devtools.
131+
// cloudtrace.v2#google.devtools.cloudtrace.v2.TraceService
132+
cloudTrace.projects.traces.batchWrite(spans, (err: Error) => {
112133
if (err) {
113-
err.message = `sendTrace error: ${err.message}`;
134+
err.message = `batchWriteSpans error: ${err.message}`;
114135
this.logger.error(err.message);
115136
reject(err);
116137
} else {
117-
const successMsg = 'sendTrace sucessfully';
138+
const successMsg = 'batchWriteSpans sucessfully';
118139
this.logger.debug(successMsg);
119140
resolve(successMsg);
120141
}
@@ -124,10 +145,10 @@ export class StackdriverTraceExporter implements Exporter {
124145

125146
/**
126147
* Gets the Google Application Credentials from the environment variables,
127-
* authenticates the client and calls a method to send the traces data.
148+
* authenticates the client and calls a method to send the spans data.
128149
* @param stackdriverTraces
129150
*/
130-
private authorize(stackdriverTraces: TranslatedTrace[]) {
151+
private authorize(stackdriverSpans: Span[]) {
131152
return auth.getApplicationDefault()
132153
.then((client) => {
133154
let authClient = client.credential as JWT;
@@ -138,12 +159,12 @@ export class StackdriverTraceExporter implements Exporter {
138159
authClient = authClient.createScoped(scopes);
139160
}
140161

141-
const traces: TracesWithCredentials = {
142-
projectId: client.projectId,
143-
resource: {traces: stackdriverTraces},
162+
const spans: SpansWithCredentials = {
163+
name: `projects/${this.projectId}`,
164+
resource: {spans: stackdriverSpans},
144165
auth: authClient
145166
};
146-
return traces;
167+
return spans;
147168
})
148169
.catch((err) => {
149170
err.message = `authorize error: ${err.message}`;

0 commit comments

Comments
 (0)