From 160e0b3dc75807af86551d0959339316dbbc6b59 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Thu, 7 Feb 2019 17:17:00 -0800 Subject: [PATCH 1/2] OpenCensus Stackdriver Trace Exporter is updated to use Stackdriver Trace V2 APIs. --- CHANGELOG.md | 1 + .../src/stackdriver-cloudtrace-utils.ts | 170 +++++++++++ .../src/stackdriver-cloudtrace.ts | 119 ++++---- .../src/types.ts | 129 ++++++-- .../test/nocks.ts | 4 +- .../test/test-stackdriver-cloudtrace-utils.ts | 288 ++++++++++++++++++ .../test/test-stackdriver-cloudtrace.ts | 172 ++++++----- 7 files changed, 734 insertions(+), 149 deletions(-) create mode 100644 packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace-utils.ts create mode 100644 packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace-utils.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f01873ec9..b2f3f4989 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.** diff --git a/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace-utils.ts b/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace-utils.ts new file mode 100644 index 000000000..95880008f --- /dev/null +++ b/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace-utils.ts @@ -0,0 +1,170 @@ +/** + * 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 + * @param droppedAttributesCount number + * @returns types.Attributes + */ +export function createAttributes( + attributes: coreTypes.Attributes, + resourceLabels: Record, + 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 { + const timeEvents: types.TimeEvent[] = []; + if (annotationTimedEvents) { + annotationTimedEvents.forEach(annotation => { + timeEvents.push({ + time: new Date(annotation.timestamp).toISOString(), + annotation: { + description: stringToTruncatableString(annotation.description), + attributes: createAttributesBuilder(annotation.attributes, 0) + } + }); + }); + } + if (messageEventTimedEvents) { + messageEventTimedEvents.forEach(messageEvent => { + timeEvents.push({ + 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) { + const resource = await monitoredResource; + const resourceLabels: Record = {}; + 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 = {}; + 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': + 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; } + } +} diff --git a/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace.ts b/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace.ts index 373d3c8f0..4870a7798 100644 --- a/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace.ts +++ b/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace.ts @@ -14,17 +14,17 @@ * 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 { @@ -32,11 +32,14 @@ export class StackdriverTraceExporter implements Exporter { exporterBuffer: ExporterBuffer; logger: Logger; failBuffer: SpanContext[] = []; + private RESOURCE_LABELS: Promise>; 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)); } /** @@ -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) { @@ -70,51 +72,66 @@ 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) + private createSpan( + span: OCSpan, resourceLabels: Record): 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, + 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) => { + 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); } @@ -124,10 +141,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; @@ -138,12 +155,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}`; diff --git a/packages/opencensus-exporter-stackdriver/src/types.ts b/packages/opencensus-exporter-stackdriver/src/types.ts index bb3e37883..93ed5abf2 100644 --- a/packages/opencensus-exporter-stackdriver/src/types.ts +++ b/packages/opencensus-exporter-stackdriver/src/types.ts @@ -17,21 +17,122 @@ import {Bucket, ExporterConfig} from '@opencensus/core'; import {JWT} from 'google-auth-library'; -export type TranslatedTrace = { - projectId: string, - traceId: string, - spans: TranslatedSpan[] +export type Span = { + name?: string, + spanId?: string, + parentSpanId?: string, + displayName?: TruncatableString, + startTime?: string, + endTime?: string, + attributes?: Attributes, + stackTrace?: StackTrace, + timeEvents?: TimeEvents, + links?: Links, + status?: Status, + sameProcessAsParentSpan?: boolean, + childSpanCount?: number }; -export type TranslatedSpan = { - name: string, - kind: string, - spanId: string, - startTime: Date, - endTime: Date, - labels: Record +export type Attributes = { + attributeMap?: {[key: string]: AttributeValue;}; + droppedAttributesCount?: number; }; +export type AttributeValue = { + boolValue?: boolean; + intValue?: string; + stringValue?: TruncatableString; +}; + +export type TruncatableString = { + value?: string; + truncatedByteCount?: number; +}; + +export type Links = { + droppedLinksCount?: number; + link?: Link[]; +}; + +export type Link = { + attributes?: Attributes; + spanId?: string; + traceId?: string; + type?: LinkType; +}; + +export type StackTrace = { + stackFrames?: StackFrames; + stackTraceHashId?: string; +}; + +export type StackFrames = { + droppedFramesCount?: number; + frame?: StackFrame[]; +}; + +export type StackFrame = { + columnNumber?: string; + fileName?: TruncatableString; + functionName?: TruncatableString; + lineNumber?: string; + loadModule?: Module; + originalFunctionName?: TruncatableString; + sourceVersion?: TruncatableString; +}; + +export type Module = { + buildId?: TruncatableString; + module?: TruncatableString; +}; + +export type Status = { + code?: number; + message?: string; +}; + +export type TimeEvents = { + droppedAnnotationsCount?: number; + droppedMessageEventsCount?: number; + timeEvent?: TimeEvent[]; +}; + +export type TimeEvent = { + annotation?: Annotation; + messageEvent?: MessageEvent; + time?: string; +}; + +export type Annotation = { + attributes?: Attributes; + description?: TruncatableString; +}; + +export type MessageEvent = { + id?: string; + type?: Type; + compressedSizeBytes?: string; + uncompressedSizeBytes?: string; +}; + +export enum Type { + TYPE_UNSPECIFIED = 0, + SENT = 1, + RECEIVED = 2 +} + +export enum LinkType { + UNSPECIFIED = 0, + CHILD_LINKED_SPAN = 1, + PARENT_LINKED_SPAN = 2 +} + +export interface SpansWithCredentials { + name: string; + resource: {spans: {}}; + auth: JWT; +} + /** * Options for stackdriver configuration */ @@ -58,12 +159,6 @@ export interface StackdriverExporterOptions extends ExporterConfig { onMetricUploadError?: (err: Error) => void; } -export interface TracesWithCredentials { - projectId: string; - resource: {traces: {}}; - auth: JWT; -} - export enum MetricKind { UNSPECIFIED = 'METRIC_KIND_UNSPECIFIED', GAUGE = 'GAUGE', diff --git a/packages/opencensus-exporter-stackdriver/test/nocks.ts b/packages/opencensus-exporter-stackdriver/test/nocks.ts index ed93f79fe..58cf32830 100644 --- a/packages/opencensus-exporter-stackdriver/test/nocks.ts +++ b/packages/opencensus-exporter-stackdriver/test/nocks.ts @@ -97,13 +97,13 @@ export function hostname(status: number|(() => string), reply?: () => string) { .reply(status, reply, {'Metadata-Flavor': 'Google'}); } -export function patchTraces( +export function batchWrite( project: string, validator?: (body: T) => boolean, reply?: () => string, withError?: boolean) { validator = validator || accept; const interceptor = nock('https://cloudtrace.googleapis.com') - .patch('/v1/projects/' + project + '/traces', validator); + .post('/v2/projects/' + project + '/traces:batchWrite', validator); let scope: nock.Scope; if (withError) { scope = interceptor.replyWithError(reply); diff --git a/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace-utils.ts b/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace-utils.ts new file mode 100644 index 000000000..dca5dbce0 --- /dev/null +++ b/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace-utils.ts @@ -0,0 +1,288 @@ +/** + * 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 assert from 'assert'; +import {createAttributes, createLinks, createTimeEvents, getResourceLabels} from '../src/stackdriver-cloudtrace-utils'; + +describe('Stackdriver CloudTrace Exporter Utils', () => { + describe('createLinks()', () => { + const links: coreTypes.Link[] = [ + { + traceId: 'traceId1', + spanId: 'spanId1', + type: coreTypes.LinkType.PARENT_LINKED_SPAN, + attributes: { + 'child_link_attribute_string': 'foo1', + 'child_link_attribute_number': 123, + 'child_link_attribute_boolean': true, + } + }, + { + traceId: 'traceId2', + spanId: 'spanId2', + type: coreTypes.LinkType.CHILD_LINKED_SPAN, + attributes: {} + }, + { + traceId: 'traceId3', + spanId: 'spanId3', + type: coreTypes.LinkType.UNSPECIFIED, + attributes: {} + } + ]; + + const expectedLink = [ + { + type: 2, + traceId: 'traceId1', + spanId: 'spanId1', + attributes: { + droppedAttributesCount: 0, + attributeMap: { + child_link_attribute_string: {stringValue: {value: 'foo1'}}, + child_link_attribute_number: {intValue: '123'}, + child_link_attribute_boolean: {boolValue: true} + } + } + }, + { + type: 1, + traceId: 'traceId2', + spanId: 'spanId2', + attributes: {'attributeMap': {}, 'droppedAttributesCount': 0} + }, + { + type: 0, + traceId: 'traceId3', + spanId: 'spanId3', + attributes: {'attributeMap': {}, 'droppedAttributesCount': 0} + } + ]; + + it('should return stackdriver links', () => { + const stackdriverLinks = createLinks(links, 2); + + assert.equal(stackdriverLinks.droppedLinksCount, 2); + assert.equal(stackdriverLinks.link.length, 3); + assert.deepEqual(stackdriverLinks.link, expectedLink); + }); + }); + + describe('createTimeEvents()', () => { + const ts = 123456789; + const annotations: coreTypes.Annotation[] = [ + { + description: 'my_annotation', + timestamp: ts, + attributes: {myString: 'bar', myNumber: 123, myBoolean: true} + }, + { + description: 'my_annotation1', + timestamp: ts, + attributes: {myString: 'bar1', myNumber: 456} + }, + {description: 'my_annotation2', timestamp: ts, attributes: {}} + ]; + const messageEvents: coreTypes.MessageEvent[] = [ + {id: 'aaaa', timestamp: ts, type: coreTypes.MessageEventType.SENT}, + {id: 'ffff', timestamp: ts, type: coreTypes.MessageEventType.RECEIVED}, { + id: 'eeee', + timestamp: ts, + type: coreTypes.MessageEventType.UNSPECIFIED + } + ]; + + const expectedTimeEvent = [ + { + time: '1970-01-02T10:17:36.789Z', + annotation: { + description: {value: 'my_annotation'}, + attributes: { + attributeMap: { + myString: {stringValue: {value: 'bar'}}, + myNumber: {intValue: '123'}, + myBoolean: {boolValue: true} + }, + droppedAttributesCount: 0 + } + } + }, + { + time: '1970-01-02T10:17:36.789Z', + annotation: { + description: {value: 'my_annotation1'}, + attributes: { + attributeMap: { + myString: {stringValue: {value: 'bar1'}}, + myNumber: {intValue: '456'} + }, + droppedAttributesCount: 0 + } + } + }, + { + time: '1970-01-02T10:17:36.789Z', + annotation: { + description: {value: 'my_annotation2'}, + attributes: {attributeMap: {}, droppedAttributesCount: 0} + } + }, + { + messageEvent: { + id: 'aaaa', + type: 1, + }, + time: '1970-01-02T10:17:36.789Z', + }, + { + messageEvent: { + id: 'ffff', + type: 2, + }, + time: '1970-01-02T10:17:36.789Z', + }, + { + messageEvent: { + id: 'eeee', + type: 0, + }, + time: '1970-01-02T10:17:36.789Z', + } + ]; + + it('should return stackdriver TimeEvents', () => { + const stackdriverTimeEvents = + createTimeEvents(annotations, messageEvents, 2, 3); + + assert.equal(stackdriverTimeEvents.droppedAnnotationsCount, 2); + assert.equal(stackdriverTimeEvents.droppedMessageEventsCount, 3); + assert.equal(stackdriverTimeEvents.timeEvent.length, 6); + assert.deepEqual(stackdriverTimeEvents.timeEvent, expectedTimeEvent); + }); + }); + + describe('createAttributes()', () => { + const attributes = {'my-attribute': 100, 'my-attribute1': 'test'}; + let expectedAttributeMap = { + 'g.co/agent': + {'stringValue': {'value': `opencensus-node [${coreTypes.version}]`}}, + 'my-attribute': {'intValue': '100'}, + 'my-attribute1': {'stringValue': {'value': 'test'}} + }; + + it('should return stackdriver Attributes', () => { + const stackdriverAttribute = createAttributes(attributes, {}, 0); + assert.equal(stackdriverAttribute.droppedAttributesCount, 0); + assert.equal(Object.keys(stackdriverAttribute.attributeMap).length, 3); + assert.deepEqual(stackdriverAttribute.attributeMap, expectedAttributeMap); + }); + + it('should return stackdriver Attributes with labels', () => { + const stackdriverAttribute = createAttributes( + attributes, { + 'g.co/r/podId': {'intValue': '100'}, + 'g.co/r/project_id': {'stringValue': {'value': 'project1'}} + }, + 2); + expectedAttributeMap = Object.assign({}, expectedAttributeMap, { + 'g.co/r/podId': {'intValue': '100'}, + 'g.co/r/project_id': {'stringValue': {'value': 'project1'}} + }); + assert.equal(stackdriverAttribute.droppedAttributesCount, 2); + assert.equal(Object.keys(stackdriverAttribute.attributeMap).length, 5); + assert.deepEqual(stackdriverAttribute.attributeMap, expectedAttributeMap); + }); + }); + + describe('getResourceLabels()', () => { + it('should return K8s container labels', async () => { + const resource = { + type: 'k8s_container', + labels: { + 'container_name': 'c1', + 'namespace_name': 'default', + 'pod_name': 'pod-xyz-123', + 'project_id': 'my-project-id', + 'location': 'zone1' + } + }; + const expectedLabels = { + 'g.co/r/k8s_container/container_name': {'stringValue': {'value': 'c1'}}, + 'g.co/r/k8s_container/location': {'stringValue': {'value': 'zone1'}}, + 'g.co/r/k8s_container/namespace_name': + {'stringValue': {'value': 'default'}}, + 'g.co/r/k8s_container/pod_name': + {'stringValue': {'value': 'pod-xyz-123'}}, + 'g.co/r/k8s_container/project_id': + {'stringValue': {'value': 'my-project-id'}} + }; + + const resolvingPromise = Promise.resolve(resource); + const resourceLabels = await getResourceLabels(resolvingPromise); + assert.equal(Object.keys(resourceLabels).length, 5); + assert.deepEqual(resourceLabels, expectedLabels); + }); + + it('should return gce instance labels', async () => { + const resource = { + type: 'gce_instance', + labels: { + 'instance_id': 'instance1', + 'zone': 'zone1', + 'project_id': 'my-project-id', + } + }; + const expectedLabels = { + 'g.co/r/gce_instance/instance_id': + {'stringValue': {'value': 'instance1'}}, + 'g.co/r/gce_instance/project_id': + {'stringValue': {'value': 'my-project-id'}}, + 'g.co/r/gce_instance/zone': {'stringValue': {'value': 'zone1'}} + }; + const resolvingPromise = Promise.resolve(resource); + const resourceLabels = await getResourceLabels(resolvingPromise); + assert.equal(Object.keys(resourceLabels).length, 3); + assert.deepEqual(resourceLabels, expectedLabels); + }); + + it('should return aws ec2 instance labels', async () => { + const resource = { + type: 'aws_ec2_instance', + labels: { + 'instance_id': 'instance1', + 'region': 'region1', + 'project_id': 'my-project-id', + 'aws_account': 'my-account-id', + } + }; + const expectedLabels = { + 'g.co/r/aws_ec2_instance/aws_account': + {'stringValue': {'value': 'my-account-id'}}, + 'g.co/r/aws_ec2_instance/instance_id': + {'stringValue': {'value': 'instance1'}}, + 'g.co/r/aws_ec2_instance/project_id': + {'stringValue': {'value': 'my-project-id'}}, + 'g.co/r/aws_ec2_instance/region': + {'stringValue': {'value': 'region1'}} + }; + + const resolvingPromise = Promise.resolve(resource); + const resourceLabels = await getResourceLabels(resolvingPromise); + assert.equal(Object.keys(resourceLabels).length, 4); + assert.deepEqual(resourceLabels, expectedLabels); + }); + }); +}); diff --git a/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace.ts b/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace.ts index 386e43dff..d96dee508 100644 --- a/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace.ts +++ b/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace.ts @@ -14,56 +14,28 @@ * limitations under the License. */ -import {CoreTracer, RootSpan} from '@opencensus/core'; -import {logger} from '@opencensus/core'; +import {CoreTracer, logger, RootSpan, version} from '@opencensus/core'; import * as assert from 'assert'; -import * as fs from 'fs'; -// TODO change to use import when type package for hex2dec will be available -const {hexToDec}: {[key: string]: (input: string) => string} = - require('hex2dec'); import * as nock from 'nock'; -import {StackdriverExporterOptions, StackdriverTraceExporter, TranslatedTrace} from '../src/'; +import {Span, StackdriverExporterOptions, StackdriverTraceExporter} from '../src/'; import * as nocks from './nocks'; process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; -let PROJECT_ID = 'fake-project-id'; - -function checkEnvironoment(): boolean { - return false; -} +const PROJECT_ID = 'fake-project-id'; describe('Stackdriver Trace Exporter', function() { this.timeout(0); const testLogger = logger.logger(); - let dryrun = true; - const GOOGLE_APPLICATION_CREDENTIALS = - process.env.GOOGLE_APPLICATION_CREDENTIALS as string; - const OPENCENSUS_NETWORK_TESTS = - process.env.OPENCENSUS_NETWORK_TESTS as string; let exporterOptions: StackdriverExporterOptions; let exporter: StackdriverTraceExporter; let tracer: CoreTracer; before(() => { - if (GOOGLE_APPLICATION_CREDENTIALS) { - dryrun = !fs.existsSync(GOOGLE_APPLICATION_CREDENTIALS) && - !fs.existsSync(OPENCENSUS_NETWORK_TESTS); - if (!dryrun) { - const credentials = require(GOOGLE_APPLICATION_CREDENTIALS); - PROJECT_ID = credentials.project_id; - testLogger.debug( - 'GOOGLE_APPLICATION_CREDENTIALS: %s', - GOOGLE_APPLICATION_CREDENTIALS); - testLogger.debug('projectId = %s', PROJECT_ID); - } - } - if (dryrun) { - nock.disableNetConnect(); - } - testLogger.debug('dryrun=%s', dryrun); + nock.disableNetConnect(); + nocks.noDetectResource(); exporterOptions = { projectId: PROJECT_ID, bufferTimeout: 200, @@ -76,13 +48,8 @@ describe('Stackdriver Trace Exporter', function() { tracer = new CoreTracer(); tracer.start({samplingRate: 1}); tracer.registerSpanEventListener(exporter); - if (!dryrun) { - process.env.GOOGLE_APPLICATION_CREDENTIALS = - GOOGLE_APPLICATION_CREDENTIALS; - } }); - /* Should add spans to an exporter buffer */ describe('onEndSpan()', () => { it('should add a root span to an exporter buffer', () => { const rootSpanOptions = {name: 'sdBufferTestRootSpan'}; @@ -105,39 +72,85 @@ describe('Stackdriver Trace Exporter', function() { }); }); - /* Should export spans to stackdriver */ - describe('publish()', () => { - /* TODO: doesnt work with latest `gcloud auth application-default login` - https://github.com/census-instrumentation/opencensus-node/issues/182 - it('should fail exporting by authentication error', () => { - process.env.GOOGLE_APPLICATION_CREDENTIALS = ''; - if (dryrun) { - nocks.oauth2(body => true); - } + describe('translateSpan()', () => { + it('should translate to stackdriver spans', () => { return tracer.startRootSpan( - {name: 'sdNoExportTestRootSpan'}, async (rootSpan: RootSpan) => { - const span = tracer.startChildSpan('sdNoExportTestChildSpan'); + {name: 'root-test'}, async (rootSpan: RootSpan) => { + const span = tracer.startChildSpan('spanTest'); span.end(); rootSpan.end(); - return exporter.publish([rootSpan]).then(result => { - assert.ok(result.message.indexOf('authorize error') >= 0); - assert.strictEqual( - exporter.failBuffer[0].traceId, rootSpan.spanContext.traceId); - }); + const spanList = await exporter.translateSpan([rootSpan]); + assert.equal(spanList.length, 2); + assert.deepEqual(spanList, [ + { + 'attributes': { + 'attributeMap': { + 'g.co/agent': { + 'stringValue': + {'value': `opencensus-node [${version}]`} + } + }, + 'droppedAttributesCount': 0 + }, + 'childSpanCount': null, + 'displayName': {'value': 'root-test'}, + 'endTime': rootSpan.endTime.toISOString(), + 'links': {'droppedLinksCount': 0, 'link': []}, + 'name': `projects/fake-project-id/traces/${ + rootSpan.traceId}/spans/${rootSpan.id}`, + 'sameProcessAsParentSpan': true, + 'spanId': rootSpan.id, + 'stackTrace': null, + 'startTime': rootSpan.startTime.toISOString(), + 'status': {'code': 0}, + 'timeEvents': { + 'droppedAnnotationsCount': 0, + 'droppedMessageEventsCount': 0, + 'timeEvent': [] + } + }, + { + 'attributes': { + 'attributeMap': { + 'g.co/agent': { + 'stringValue': + {'value': `opencensus-node [${version}]`} + } + }, + 'droppedAttributesCount': 0 + }, + 'childSpanCount': null, + 'displayName': {'value': 'spanTest'}, + 'endTime': span.endTime.toISOString(), + 'links': {'droppedLinksCount': 0, 'link': []}, + 'name': `projects/fake-project-id/traces/${ + span.traceId}/spans/${span.id}`, + 'parentSpanId': rootSpan.id, + 'sameProcessAsParentSpan': true, + 'spanId': span.id, + 'stackTrace': null, + 'startTime': span.startTime.toISOString(), + 'status': {'code': 0}, + 'timeEvents': { + 'droppedAnnotationsCount': 0, + 'droppedMessageEventsCount': 0, + 'timeEvent': [] + }, + } + ]); }); }); - */ + }); + describe('publish()', () => { it('should fail exporting with wrong projectId', () => { + nock.enableNetConnect(); const NOEXIST_PROJECT_ID = 'no-existent-project-id-99999'; - if (dryrun) { - process.env.GOOGLE_APPLICATION_CREDENTIALS = - __dirname + '/fixtures/fakecredentials.json'; - nocks.oauth2(body => true); - nocks.patchTraces(NOEXIST_PROJECT_ID, null, null, false); - } + process.env.GOOGLE_APPLICATION_CREDENTIALS = + __dirname + '/fixtures/fakecredentials.json'; + nocks.oauth2(body => true); const failExporterOptions = { projectId: NOEXIST_PROJECT_ID, logger: logger.logger('debug') @@ -153,7 +166,10 @@ describe('Stackdriver Trace Exporter', function() { rootSpan.end(); return failExporter.publish([rootSpan]).then(result => { - assert.ok(result.message.indexOf('sendTrace error: ') >= 0); + assert.ok( + result.message.indexOf( + 'batchWriteSpans error: Request had invalid authentication credentials.') >= + 0); assert.strictEqual( failExporter.failBuffer[0].traceId, @@ -167,33 +183,29 @@ describe('Stackdriver Trace Exporter', function() { {name: 'sdExportTestRootSpan'}, async (rootSpan: RootSpan) => { const span = tracer.startChildSpan('sdExportTestChildSpan'); - if (dryrun) { - nocks.oauth2(body => true); - nocks.patchTraces( - PROJECT_ID, (body: {traces: TranslatedTrace[]}): boolean => { - assert.strictEqual(body.traces.length, 1); - const {spans} = body.traces[0]; - assert.strictEqual(spans.length, 2); - assert.strictEqual(hexToDec(rootSpan.id), spans[1].spanId); - assert.strictEqual(hexToDec(span.id), spans[0].spanId); - return true; - }, null, false); - } - + nocks.oauth2(body => true); + nocks.batchWrite(PROJECT_ID, (body: {spans: Span[]}): boolean => { + assert.strictEqual(body.spans.length, 2); + const spans = body.spans; + assert.strictEqual(spans[0].spanId, rootSpan.id); + assert.strictEqual(spans[1].spanId, span.id); + return true; + }, null, false); span.end(); rootSpan.end(); return exporter.publish([rootSpan]).then(result => { - assert.ok(result.indexOf('sendTrace sucessfully') >= 0); + assert.ok(result.indexOf('batchWriteSpans sucessfully') >= 0); }); }); }); it('should fail exporting by network error', async () => { nock('https://cloudtrace.googleapis.com') - .persist() .intercept( - '/v1/projects/' + exporterOptions.projectId + '/traces', 'patch') + '/v2/projects/' + exporterOptions.projectId + + '/traces:batchWrite', + 'post') .reply(443, 'Simulated Network Error'); nocks.oauth2(body => true); @@ -205,7 +217,9 @@ describe('Stackdriver Trace Exporter', function() { rootSpan.end(); return exporter.publish([rootSpan]).then(result => { - assert.ok(result.message.indexOf('Simulated Network Error') >= 0); + assert.ok( + result.message.indexOf( + 'batchWriteSpans error: Simulated Network Error') >= 0); }); }); }); From 4881ff797462fadc7a879b7825b8ad8e61c67f88 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Mon, 11 Feb 2019 12:28:20 -0800 Subject: [PATCH 2/2] fix review comments --- .../src/stackdriver-cloudtrace-utils.ts | 37 +++++++++---------- .../src/stackdriver-cloudtrace.ts | 8 +++- .../test/test-stackdriver-cloudtrace-utils.ts | 9 +++++ .../test/test-stackdriver-cloudtrace.ts | 2 +- 4 files changed, 34 insertions(+), 22 deletions(-) diff --git a/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace-utils.ts b/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace-utils.ts index 95880008f..5a0a63bbc 100644 --- a/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace-utils.ts +++ b/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace-utils.ts @@ -64,28 +64,26 @@ export function createTimeEvents( messageEventTimedEvents: coreTypes.MessageEvent[], droppedAnnotationsCount: number, droppedMessageEventsCount: number): types.TimeEvents { - const timeEvents: types.TimeEvent[] = []; + let timeEvents: types.TimeEvent[] = []; if (annotationTimedEvents) { - annotationTimedEvents.forEach(annotation => { - timeEvents.push({ - time: new Date(annotation.timestamp).toISOString(), - annotation: { - description: stringToTruncatableString(annotation.description), - attributes: createAttributesBuilder(annotation.attributes, 0) - } - }); - }); + timeEvents = annotationTimedEvents.map( + (annotation) => ({ + time: new Date(annotation.timestamp).toISOString(), + annotation: { + description: stringToTruncatableString(annotation.description), + attributes: createAttributesBuilder(annotation.attributes, 0) + } + })); } if (messageEventTimedEvents) { - messageEventTimedEvents.forEach(messageEvent => { - timeEvents.push({ - time: new Date(messageEvent.timestamp).toISOString(), - messageEvent: { - id: messageEvent.id, - type: createMessageEventType(messageEvent.type) - } - }); - }); + timeEvents.push(...messageEventTimedEvents.map( + (messageEvent) => ({ + time: new Date(messageEvent.timestamp).toISOString(), + messageEvent: { + id: messageEvent.id, + type: createMessageEventType(messageEvent.type) + } + }))); } return { timeEvent: timeEvents, @@ -135,6 +133,7 @@ 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}; diff --git a/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace.ts b/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace.ts index 4870a7798..0727f08e9 100644 --- a/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace.ts +++ b/packages/opencensus-exporter-stackdriver/src/stackdriver-cloudtrace.ts @@ -105,8 +105,8 @@ export class StackdriverTraceExporter implements Exporter { links: createLinks(span.links, span.droppedLinksCount), status: {code: span.status.code}, sameProcessAsParentSpan: !span.remoteParent, - childSpanCount: null, - stackTrace: null, // Unsupported by nodejs + childSpanCount: null, // TODO: Consider to add count after pull/332 + stackTrace: null, // Unsupported by nodejs }; if (span.parentSpanId) { spanBuilder.parentSpanId = span.parentSpanId; @@ -125,6 +125,10 @@ export class StackdriverTraceExporter implements Exporter { */ private batchWriteSpans(spans: SpansWithCredentials) { return new Promise((resolve, reject) => { + // 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 = `batchWriteSpans error: ${err.message}`; diff --git a/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace-utils.ts b/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace-utils.ts index dca5dbce0..9726aa1d8 100644 --- a/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace-utils.ts +++ b/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace-utils.ts @@ -172,6 +172,15 @@ describe('Stackdriver CloudTrace Exporter Utils', () => { assert.equal(stackdriverTimeEvents.timeEvent.length, 6); assert.deepEqual(stackdriverTimeEvents.timeEvent, expectedTimeEvent); }); + + it('should return stackdriver TimeEvents when empty annotations and messageEvents', + () => { + const stackdriverTimeEvents = createTimeEvents([], [], 0, 0); + + assert.equal(stackdriverTimeEvents.droppedAnnotationsCount, 0); + assert.equal(stackdriverTimeEvents.droppedMessageEventsCount, 0); + assert.equal(stackdriverTimeEvents.timeEvent.length, 0); + }); }); describe('createAttributes()', () => { diff --git a/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace.ts b/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace.ts index d96dee508..85ea3e1e1 100644 --- a/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace.ts +++ b/packages/opencensus-exporter-stackdriver/test/test-stackdriver-cloudtrace.ts @@ -35,7 +35,6 @@ describe('Stackdriver Trace Exporter', function() { before(() => { nock.disableNetConnect(); - nocks.noDetectResource(); exporterOptions = { projectId: PROJECT_ID, bufferTimeout: 200, @@ -44,6 +43,7 @@ describe('Stackdriver Trace Exporter', function() { }); beforeEach(() => { + nocks.noDetectResource(); exporter = new StackdriverTraceExporter(exporterOptions); tracer = new CoreTracer(); tracer.start({samplingRate: 1});