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

Commit

Permalink
Exporter/Stackdriver: Use Trace SDKs (v2 API) (#338)
Browse files Browse the repository at this point in the history
* OpenCensus Stackdriver Trace Exporter is updated to use Stackdriver Trace V2 APIs.

* fix review comments
  • Loading branch information
mayurkale22 committed Feb 12, 2019
1 parent 515789e commit 55a5545
Show file tree
Hide file tree
Showing 7 changed files with 746 additions and 149 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file.
- Add ```opencensus-resource-util``` to auto detect AWS, GCE and Kubernetes(K8S) monitored resource, based on the environment where the application is running.
- Add optional `uncompressedSize` and `compressedSize` fields to `MessageEvent` interface.
- Add a ```setStatus``` method in the Span.
- OpenCensus Stackdriver Trace Exporter is updated to use Stackdriver Trace V2 APIs.

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
/**
* Copyright 2019, OpenCensus 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.
*/

import * as coreTypes from '@opencensus/core';
import * as types from './types';

const AGENT_LABEL_KEY = 'g.co/agent';
const AGENT_LABEL_VALUE_STRING = `opencensus-node [${coreTypes.version}]`;
const AGENT_LABEL_VALUE = createAttributeValue(AGENT_LABEL_VALUE_STRING);

/**
* Creates StackDriver Links from OpenCensus Link.
* @param links coreTypes.Link[]
* @param droppedLinksCount number
* @returns types.Links
*/
export function createLinks(
links: coreTypes.Link[], droppedLinksCount: number): types.Links {
return {link: links.map((link) => createLink(link)), droppedLinksCount};
}

/**
* Creates StackDriver Attributes from OpenCensus Attributes.
* @param attributes coreTypes.Attributes
* @param resourceLabels Record<string, types.AttributeValue>
* @param droppedAttributesCount number
* @returns types.Attributes
*/
export function createAttributes(
attributes: coreTypes.Attributes,
resourceLabels: Record<string, types.AttributeValue>,
droppedAttributesCount: number): types.Attributes {
const attributesBuilder =
createAttributesBuilder(attributes, droppedAttributesCount);
attributesBuilder.attributeMap[AGENT_LABEL_KEY] = AGENT_LABEL_VALUE;
attributesBuilder.attributeMap =
Object.assign({}, attributesBuilder.attributeMap, resourceLabels);
return attributesBuilder;
}

/**
* Creates StackDriver TimeEvents from OpenCensus Annotation and MessageEvent.
* @param annotationTimedEvents coreTypes.Annotation[]
* @param messageEventTimedEvents coreTypes.MessageEvent[]
* @param droppedAnnotationsCount number
* @param droppedMessageEventsCount number
* @returns types.TimeEvents
*/
export function createTimeEvents(
annotationTimedEvents: coreTypes.Annotation[],
messageEventTimedEvents: coreTypes.MessageEvent[],
droppedAnnotationsCount: number,
droppedMessageEventsCount: number): types.TimeEvents {
let timeEvents: types.TimeEvent[] = [];
if (annotationTimedEvents) {
timeEvents = annotationTimedEvents.map(
(annotation) => ({
time: new Date(annotation.timestamp).toISOString(),
annotation: {
description: stringToTruncatableString(annotation.description),
attributes: createAttributesBuilder(annotation.attributes, 0)
}
}));
}
if (messageEventTimedEvents) {
timeEvents.push(...messageEventTimedEvents.map(
(messageEvent) => ({
time: new Date(messageEvent.timestamp).toISOString(),
messageEvent: {
id: messageEvent.id,
type: createMessageEventType(messageEvent.type)
}
})));
}
return {
timeEvent: timeEvents,
droppedAnnotationsCount,
droppedMessageEventsCount
};
}

export function stringToTruncatableString(value: string):
types.TruncatableString {
return {value};
}

export async function getResourceLabels(
monitoredResource: Promise<types.MonitoredResource>) {
const resource = await monitoredResource;
const resourceLabels: Record<string, types.AttributeValue> = {};
if (resource.type === 'global') {
return resourceLabels;
}
for (const key of Object.keys(resource.labels)) {
const resourceLabel = `g.co/r/${resource.type}/${key}`;
resourceLabels[resourceLabel] = createAttributeValue(resource.labels[key]);
}
return resourceLabels;
}

function createAttributesBuilder(
attributes: coreTypes.Attributes,
droppedAttributesCount: number): types.Attributes {
const attributeMap: Record<string, types.AttributeValue> = {};
for (const key of Object.keys(attributes)) {
attributeMap[key] = createAttributeValue(attributes[key]);
}
return {attributeMap, droppedAttributesCount};
}

function createLink(link: coreTypes.Link): types.Link {
const traceId = link.traceId;
const spanId = link.spanId;
const type = createLinkType(link.type);
const attributes = createAttributesBuilder(link.attributes, 0);
return {traceId, spanId, type, attributes};
}

function createAttributeValue(value: string|number|
boolean): types.AttributeValue {
switch (typeof value) {
case 'number':
// TODO: Consider to change to doubleValue when available in V2 API.
return {intValue: String(value)};
case 'boolean':
return {boolValue: value as boolean};
case 'string':
return {stringValue: stringToTruncatableString(value)};
default:
throw new Error(`Unsupported type : ${typeof value}`);
}
}

function createMessageEventType(type: coreTypes.MessageEventType) {
switch (type) {
case coreTypes.MessageEventType.SENT: {
return types.Type.SENT;
}
case coreTypes.MessageEventType.RECEIVED: {
return types.Type.RECEIVED;
}
default: { return types.Type.TYPE_UNSPECIFIED; }
}
}

function createLinkType(type: coreTypes.LinkType) {
switch (type) {
case coreTypes.LinkType.CHILD_LINKED_SPAN: {
return types.LinkType.CHILD_LINKED_SPAN;
}
case coreTypes.LinkType.PARENT_LINKED_SPAN: {
return types.LinkType.PARENT_LINKED_SPAN;
}
default: { return types.LinkType.UNSPECIFIED; }
}
}
123 changes: 72 additions & 51 deletions packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,32 @@
* limitations under the License.
*/

import {Exporter, ExporterBuffer, RootSpan, Span, SpanContext} from '@opencensus/core';
import {Exporter, ExporterBuffer, RootSpan, Span as OCSpan, SpanContext} from '@opencensus/core';
import {logger, Logger} from '@opencensus/core';
import {auth, JWT} from 'google-auth-library';
import {google} from 'googleapis';
// TODO change to use import when types for hex2dec will be available
const {hexToDec}: {[key: string]: (input: string) => string} =
require('hex2dec');
import {StackdriverExporterOptions, TracesWithCredentials, TranslatedSpan, TranslatedTrace} from './types';

import {getDefaultResource} from './common-utils';
import {createAttributes, createLinks, createTimeEvents, getResourceLabels, stringToTruncatableString} from './stackdriver-cloudtrace-utils';
import {AttributeValue, Span, SpansWithCredentials, StackdriverExporterOptions} from './types';

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

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

constructor(options: StackdriverExporterOptions) {
this.projectId = options.projectId;
this.logger = options.logger || logger.logger();
this.exporterBuffer = new ExporterBuffer(this, options);
this.RESOURCE_LABELS =
getResourceLabels(getDefaultResource(this.projectId));
}

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

return this.authorize(stackdriverTraces)
.then((traces: TracesWithCredentials) => {
return this.sendTrace(traces);
return this.authorize(spanList)
.then((spans: SpansWithCredentials) => {
return this.batchWriteSpans(spans);
})
.catch(err => {
for (const root of rootSpans) {
Expand All @@ -70,51 +72,70 @@ export class StackdriverTraceExporter implements Exporter {
});
}

/**
* Translates root span data to Stackdriver's trace format.
* @param root
*/
private translateTrace(root: RootSpan): TranslatedTrace {
const spanList = root.spans.map((span: Span) => this.translateSpan(span));
spanList.push(this.translateSpan(root));

return {projectId: this.projectId, traceId: root.traceId, spans: spanList};
async translateSpan(rootSpans: RootSpan[]) {
const resourceLabel = await this.RESOURCE_LABELS;
const spanList: Span[] = [];
rootSpans.forEach(rootSpan => {
// RootSpan data
spanList.push(this.createSpan(rootSpan, resourceLabel));
rootSpan.spans.forEach(span => {
// Builds spans data
spanList.push(this.createSpan(span, resourceLabel));
});
});
return spanList;
}

/**
* Translates span data to Stackdriver's span format.
* @param span
*/
private translateSpan(span: Span): TranslatedSpan {
return {
name: span.name,
kind: 'SPAN_KIND_UNSPECIFIED',
spanId: hexToDec(span.id),
startTime: span.startTime,
endTime: span.endTime,
labels: Object.keys(span.attributes)
.reduce(
(acc, k) => {
acc[k] = String(span.attributes[k]);
return acc;
},
{} as Record<string, string>)
private createSpan(
span: OCSpan, resourceLabels: Record<string, AttributeValue>): Span {
const spanName =
`projects/${this.projectId}/traces/${span.traceId}/spans/${span.id}`;

const spanBuilder: Span = {
name: spanName,
spanId: span.id,
displayName: stringToTruncatableString(span.name),
startTime: span.startTime.toISOString(),
endTime: span.endTime.toISOString(),
attributes: createAttributes(
span.attributes, resourceLabels, span.droppedAttributesCount),
timeEvents: createTimeEvents(
span.annotations, span.messageEvents, span.droppedAnnotationsCount,
span.droppedMessageEventsCount),
links: createLinks(span.links, span.droppedLinksCount),
status: {code: span.status.code},
sameProcessAsParentSpan: !span.remoteParent,
childSpanCount: null, // TODO: Consider to add count after pull/332
stackTrace: null, // Unsupported by nodejs
};
if (span.parentSpanId) {
spanBuilder.parentSpanId = span.parentSpanId;
}
if (span.status.message) {
spanBuilder.status.message = span.status.message;
}

return spanBuilder;
}

/**
* Sends traces in the Stackdriver format to the service.
* @param traces
* Sends new spans to new or existing traces in the Stackdriver format to the
* service.
* @param spans
*/
private sendTrace(traces: TracesWithCredentials) {
private batchWriteSpans(spans: SpansWithCredentials) {
return new Promise((resolve, reject) => {
cloudTrace.projects.patchTraces(traces, (err: Error) => {
// TODO: Consider to use gRPC call (BatchWriteSpansRequest) for sending
// data to backend :
// https://cloud.google.com/trace/docs/reference/v2/rpc/google.devtools.
// cloudtrace.v2#google.devtools.cloudtrace.v2.TraceService
cloudTrace.projects.traces.batchWrite(spans, (err: Error) => {
if (err) {
err.message = `sendTrace error: ${err.message}`;
err.message = `batchWriteSpans error: ${err.message}`;
this.logger.error(err.message);
reject(err);
} else {
const successMsg = 'sendTrace sucessfully';
const successMsg = 'batchWriteSpans sucessfully';
this.logger.debug(successMsg);
resolve(successMsg);
}
Expand All @@ -124,10 +145,10 @@ export class StackdriverTraceExporter implements Exporter {

/**
* Gets the Google Application Credentials from the environment variables,
* authenticates the client and calls a method to send the traces data.
* authenticates the client and calls a method to send the spans data.
* @param stackdriverTraces
*/
private authorize(stackdriverTraces: TranslatedTrace[]) {
private authorize(stackdriverSpans: Span[]) {
return auth.getApplicationDefault()
.then((client) => {
let authClient = client.credential as JWT;
Expand All @@ -138,12 +159,12 @@ export class StackdriverTraceExporter implements Exporter {
authClient = authClient.createScoped(scopes);
}

const traces: TracesWithCredentials = {
projectId: client.projectId,
resource: {traces: stackdriverTraces},
const spans: SpansWithCredentials = {
name: `projects/${this.projectId}`,
resource: {spans: stackdriverSpans},
auth: authClient
};
return traces;
return spans;
})
.catch((err) => {
err.message = `authorize error: ${err.message}`;
Expand Down
Loading

0 comments on commit 55a5545

Please sign in to comment.