diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..50b83a59cd --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "packages/opentelemetry-exporter-collector/src/platform/node/protos"] + path = packages/opentelemetry-exporter-collector/src/platform/node/protos + url = git@github.com:open-telemetry/opentelemetry-proto.git diff --git a/packages/opentelemetry-exporter-collector/README.md b/packages/opentelemetry-exporter-collector/README.md index 275674470c..6d27c508c1 100644 --- a/packages/opentelemetry-exporter-collector/README.md +++ b/packages/opentelemetry-exporter-collector/README.md @@ -37,6 +37,7 @@ const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tra const { CollectorExporter } = require('@opentelemetry/exporter-collector'); const collectorOptions = { + serviceName: 'basic-service', url: '' // url is optional and can be omitted - default is http://localhost:55678/v1/trace }; diff --git a/packages/opentelemetry-exporter-collector/package.json b/packages/opentelemetry-exporter-collector/package.json index a14961fe54..5b1e97497d 100644 --- a/packages/opentelemetry-exporter-collector/package.json +++ b/packages/opentelemetry-exporter-collector/package.json @@ -16,13 +16,16 @@ "codecov:browser": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", "precompile": "tsc --version", "compile": "npm run version:update && tsc -p .", + "postcompile": "npm run submodule && npm run protos:copy", "prepare": "npm run compile", + "protos:copy": "cpx src/platform/node/protos/opentelemetry/**/*.* build/src/platform/node/protos/opentelemetry", + "submodule": "git submodule sync --recursive && git submodule update --init --recursive", "tdd": "npm run test -- --watch-extensions ts --watch", "tdd:browser": "karma start", "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts' --exclude 'test/browser/**/*.ts'", "test:browser": "nyc karma start --single-run", "version:update": "node ../../scripts/version-update.js", - "watch": "tsc -w" + "watch": "npm run protos:copy && tsc -w" }, "keywords": [ "opentelemetry", @@ -56,6 +59,7 @@ "@types/webpack-env": "1.13.9", "babel-loader": "^8.0.6", "codecov": "^3.1.0", + "cpx": "^1.5.0", "gts": "^1.0.0", "istanbul-instrumenter-loader": "^3.0.1", "karma": "^4.4.1", @@ -79,10 +83,13 @@ "webpack-merge": "^4.2.2" }, "dependencies": { + "@grpc/proto-loader": "^0.5.3", "@opentelemetry/api": "^0.6.1", "@opentelemetry/base": "^0.6.1", "@opentelemetry/core": "^0.6.1", "@opentelemetry/resources": "^0.6.1", - "@opentelemetry/tracing": "^0.6.1" + "@opentelemetry/tracing": "^0.6.1", + "google-protobuf": "^3.11.4", + "grpc": "^1.24.2" } } diff --git a/packages/opentelemetry-exporter-collector/src/CollectorExporter.ts b/packages/opentelemetry-exporter-collector/src/CollectorExporter.ts index 2bbc989cc8..7199848da8 100644 --- a/packages/opentelemetry-exporter-collector/src/CollectorExporter.ts +++ b/packages/opentelemetry-exporter-collector/src/CollectorExporter.ts @@ -18,10 +18,8 @@ import { ExportResult } from '@opentelemetry/base'; import { NoopLogger } from '@opentelemetry/core'; import { ReadableSpan, SpanExporter } from '@opentelemetry/tracing'; import { Attributes, Logger } from '@opentelemetry/api'; -import * as collectorTypes from './types'; -import { toCollectorSpan, toCollectorResource } from './transform'; import { onInit, onShutdown, sendSpans } from './platform/index'; -import { Resource } from '@opentelemetry/resources'; +import { opentelemetryProto } from './types'; /** * Collector Exporter Config @@ -65,7 +63,7 @@ export class CollectorExporter implements SpanExporter { this.shutdown = this.shutdown.bind(this); // platform dependent - onInit(this.shutdown); + onInit(this); } /** @@ -81,33 +79,34 @@ export class CollectorExporter implements SpanExporter { resultCallback(ExportResult.FAILED_NOT_RETRYABLE); return; } + this._exportSpans(spans) .then(() => { resultCallback(ExportResult.SUCCESS); }) - .catch((status: number = 0) => { - if (status < 500) { - resultCallback(ExportResult.FAILED_NOT_RETRYABLE); - } else { - resultCallback(ExportResult.FAILED_RETRYABLE); + .catch( + ( + error: opentelemetryProto.collector.trace.v1.ExportTraceServiceError + ) => { + if (error.message) { + this.logger.error(error.message); + } + if (error.code && error.code < 500) { + resultCallback(ExportResult.FAILED_NOT_RETRYABLE); + } else { + resultCallback(ExportResult.FAILED_RETRYABLE); + } } - }); + ); } private _exportSpans(spans: ReadableSpan[]): Promise { return new Promise((resolve, reject) => { try { - const spansToBeSent: collectorTypes.Span[] = spans.map(span => - toCollectorSpan(span) - ); - this.logger.debug('spans to be sent', spansToBeSent); - const resource = toCollectorResource( - spansToBeSent.length > 0 ? spans[0].resource : Resource.empty() - ); - + this.logger.debug('spans to be sent', spans); // Send spans to [opentelemetry collector]{@link https://github.com/open-telemetry/opentelemetry-collector} // it will use the appropriate transport layer automatically depends on platform - sendSpans(spansToBeSent, resolve, reject, this, resource); + sendSpans(spans, resolve, reject, this); } catch (e) { reject(e); } @@ -126,6 +125,6 @@ export class CollectorExporter implements SpanExporter { this.logger.debug('shutdown started'); // platform dependent - onShutdown(this.shutdown); + onShutdown(this); } } diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/sendSpans.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/sendSpans.ts index a41faa3bf6..a718cf17d4 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/browser/sendSpans.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/sendSpans.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2019, OpenTelemetry Authors + * Copyright 2020, OpenTelemetry Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,26 +14,26 @@ * limitations under the License. */ -import * as core from '@opentelemetry/core'; import { Logger } from '@opentelemetry/api'; +import { ReadableSpan } from '@opentelemetry/tracing'; import { CollectorExporter } from '../../CollectorExporter'; +import { toCollectorExportTraceServiceRequest } from '../../transform'; import * as collectorTypes from '../../types'; -import { VERSION } from '../../version'; /** * function that is called once when {@link ExporterCollector} is initialised - * @param shutdownF shutdown method of {@link ExporterCollector} + * @param collectorExporter CollectorExporter {@link ExporterCollector} */ -export function onInit(shutdownF: EventListener) { - window.addEventListener('unload', shutdownF); +export function onInit(collectorExporter: CollectorExporter) { + window.addEventListener('unload', collectorExporter.shutdown); } /** * function to be called once when {@link ExporterCollector} is shutdown - * @param shutdownF - shutdown method of {@link ExporterCollector} + * @param collectorExporter CollectorExporter {@link ExporterCollector} */ -export function onShutdown(shutdownF: EventListener) { - window.removeEventListener('unload', shutdownF); +export function onShutdown(collectorExporter: CollectorExporter) { + window.removeEventListener('unload', collectorExporter.shutdown); } /** @@ -43,34 +43,17 @@ export function onShutdown(shutdownF: EventListener) { * @param onSuccess * @param onError * @param collectorExporter - * @param resource */ export function sendSpans( - spans: collectorTypes.Span[], + spans: ReadableSpan[], onSuccess: () => void, - onError: (status?: number) => void, - collectorExporter: CollectorExporter, - resource: collectorTypes.Resource + onError: (error: collectorTypes.CollectorExporterError) => void, + collectorExporter: CollectorExporter ) { - const exportTraceServiceRequest: collectorTypes.ExportTraceServiceRequest = { - node: { - identifier: { - hostName: collectorExporter.hostName || window.location.host, - startTimestamp: core.hrTimeToTimeStamp(core.hrTime()), - }, - libraryInfo: { - language: collectorTypes.LibraryInfoLanguage.WEB_JS, - coreLibraryVersion: core.VERSION, - exporterVersion: VERSION, - }, - serviceInfo: { - name: collectorExporter.serviceName, - }, - attributes: collectorExporter.attributes, - }, - resource, + const exportTraceServiceRequest = toCollectorExportTraceServiceRequest( spans, - }; + collectorExporter + ); const body = JSON.stringify(exportTraceServiceRequest); @@ -104,7 +87,7 @@ export function sendSpans( function sendSpansWithBeacon( body: string, onSuccess: () => void, - onError: (status?: number) => void, + onError: (error: collectorTypes.CollectorExporterError) => void, logger: Logger, collectorUrl: string ) { @@ -113,7 +96,7 @@ function sendSpansWithBeacon( onSuccess(); } else { logger.error('sendBeacon - cannot send', body); - onError(); + onError({}); } } @@ -129,13 +112,15 @@ function sendSpansWithBeacon( function sendSpansWithXhr( body: string, onSuccess: () => void, - onError: (status?: number) => void, + onError: (error: collectorTypes.CollectorExporterError) => void, logger: Logger, collectorUrl: string ) { const xhr = new XMLHttpRequest(); xhr.open('POST', collectorUrl); xhr.setRequestHeader(collectorTypes.OT_REQUEST_HEADER, '1'); + xhr.setRequestHeader('Accept', 'application/json'); + xhr.setRequestHeader('Content-Type', 'application/json'); xhr.send(body); xhr.onreadystatechange = () => { @@ -144,8 +129,12 @@ function sendSpansWithXhr( logger.debug('xhr success', body); onSuccess(); } else { - logger.error('xhr error', xhr.status, body); - onError(xhr.status); + logger.error('body', body); + logger.error('xhr error', xhr); + onError({ + code: xhr.status, + message: xhr.responseText, + }); } } }; diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/README.md b/packages/opentelemetry-exporter-collector/src/platform/node/README.md new file mode 100644 index 0000000000..aee8555b64 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/node/README.md @@ -0,0 +1,45 @@ +### Important! +**Submodule is always pointing to certain revision number. So updating the master of the submodule repo will not have impact on your code. +Knowing this if you want to change the submodule to point to a different version (when for example proto has changed) here is how to do it:** + +### Updating submodule to point to certain revision number + +1. Make sure you are in the same folder as this instruction + +2. Update your submodules by running this command + +```shell script + git submodule sync --recursive + git submodule update --init --recursive +``` + +3. Find the SHA which you want to update to and copy it (the long one) +the latest sha when this guide was written is `e6c3c4a74d57f870a0d781bada02cb2b2c497d14` + +4. Enter a submodule directory from this directory + +```shell script + cd protos +``` + +5. Updates files in the submodule tree to given commit: + +```shell script + git checkout -q +``` + +6. Return to the main directory: + +```shell script + cd ../ +``` + +7. Please run `git status` you should see something like `Head detached at`. This is correct, go to next step + +8. Now thing which is very important. You have to commit this to apply these changes + +```shell script + git commit -am "chore: updating submodule for opentelemetry-proto" +``` + +9. If you look now at git log you will notice that the folder `protos` has been changed and it will show what was the previous sha and what is current one diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/protos b/packages/opentelemetry-exporter-collector/src/platform/node/protos new file mode 160000 index 0000000000..e6c3c4a74d --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/node/protos @@ -0,0 +1 @@ +Subproject commit e6c3c4a74d57f870a0d781bada02cb2b2c497d14 diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/sendSpans.ts b/packages/opentelemetry-exporter-collector/src/platform/node/sendSpans.ts index 18faf59bce..5b7c9035b7 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/sendSpans.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/sendSpans.ts @@ -14,31 +14,86 @@ * limitations under the License. */ -import * as http from 'http'; -import * as https from 'https'; +import * as protoLoader from '@grpc/proto-loader'; +import { ReadableSpan } from '@opentelemetry/tracing'; +import * as grpc from 'grpc'; +import * as path from 'path'; -import { IncomingMessage } from 'http'; -import * as core from '@opentelemetry/core'; import { CollectorExporter } from '../../CollectorExporter'; - import * as collectorTypes from '../../types'; +import { toCollectorExportTraceServiceRequest } from '../../transform'; +import { CollectorData, GRPCQueueItem } from './types'; +import { removeProtocol } from './util'; -import * as url from 'url'; -import { VERSION } from '../../version'; +const traceServiceClients: WeakMap< + CollectorExporter, + CollectorData +> = new WeakMap(); /** * function that is called once when {@link ExporterCollector} is initialised - * in node version this is not used - * @param shutdownF shutdown method of {@link ExporterCollector} + * @param collectorExporter CollectorExporter {@link ExporterCollector} */ -export function onInit(shutdownF: Function) {} +export function onInit(collectorExporter: CollectorExporter) { + traceServiceClients.set(collectorExporter, { + isShutDown: false, + grpcSpansQueue: [], + }); + const serverAddress = removeProtocol(collectorExporter.url); + const credentials: grpc.ChannelCredentials = grpc.credentials.createInsecure(); + + const traceServiceProtoPath = + 'opentelemetry/proto/collector/trace/v1/trace_service.proto'; + const includeDirs = [path.resolve(__dirname, 'protos')]; + + protoLoader + .load(traceServiceProtoPath, { + keepCase: false, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs, + }) + .then(packageDefinition => { + const packageObject: any = grpc.loadPackageDefinition(packageDefinition); + const exporter = traceServiceClients.get(collectorExporter); + if (!exporter) { + return; + } + exporter.traceServiceClient = new packageObject.opentelemetry.proto.collector.trace.v1.TraceService( + serverAddress, + credentials + ); + if (exporter.grpcSpansQueue.length > 0) { + const queue = exporter.grpcSpansQueue.splice(0); + queue.forEach((item: GRPCQueueItem) => { + sendSpans( + item.spans, + item.onSuccess, + item.onError, + collectorExporter + ); + }); + } + }); +} /** * function to be called once when {@link ExporterCollector} is shutdown - * in node version this is not used - * @param shutdownF - shutdown method of {@link ExporterCollector} + * @param collectorExporter CollectorExporter {@link ExporterCollector} */ -export function onShutdown(shutdownF: Function) {} +export function onShutdown(collectorExporter: CollectorExporter) { + const exporter = traceServiceClients.get(collectorExporter); + if (!exporter) { + return; + } + exporter.isShutDown = true; + + if (exporter.traceServiceClient) { + exporter.traceServiceClient.close(); + } +} /** * function to send spans to the [opentelemetry collector]{@link https://github.com/open-telemetry/opentelemetry-collector} @@ -47,75 +102,44 @@ export function onShutdown(shutdownF: Function) {} * @param onSuccess * @param onError * @param collectorExporter - * @param resource */ export function sendSpans( - spans: collectorTypes.Span[], + spans: ReadableSpan[], onSuccess: () => void, - onError: (status?: number) => void, - collectorExporter: CollectorExporter, - resource: collectorTypes.Resource + onError: (error: collectorTypes.CollectorExporterError) => void, + collectorExporter: CollectorExporter ) { - const exportTraceServiceRequest = toCollectorTraceServiceRequest( - spans, - collectorExporter, - resource - ); - const body = JSON.stringify(exportTraceServiceRequest); - const parsedUrl = url.parse(collectorExporter.url); - - const options = { - hostname: parsedUrl.hostname, - port: parsedUrl.port, - path: parsedUrl.path, - method: 'POST', - headers: { - 'Content-Length': Buffer.byteLength(body), - [collectorTypes.OT_REQUEST_HEADER]: 1, - }, - }; - - const request = parsedUrl.protocol === 'http:' ? http.request : https.request; - const req = request(options, (res: IncomingMessage) => { - if (res.statusCode && res.statusCode < 299) { - collectorExporter.logger.debug(`statusCode: ${res.statusCode}`); - onSuccess(); - } else { - collectorExporter.logger.error(`statusCode: ${res.statusCode}`); - onError(res.statusCode); - } - }); - - req.on('error', (error: Error) => { - collectorExporter.logger.error('error', error.message); - onError(); - }); - req.write(body); - req.end(); -} + const exporter = traceServiceClients.get(collectorExporter); + if (!exporter || exporter.isShutDown) { + return; + } + if (exporter.traceServiceClient) { + const exportTraceServiceRequest = toCollectorExportTraceServiceRequest( + spans, + collectorExporter + ); -export function toCollectorTraceServiceRequest( - spans: collectorTypes.Span[], - collectorExporter: CollectorExporter, - resource: collectorTypes.Resource -): collectorTypes.ExportTraceServiceRequest { - return { - node: { - identifier: { - hostName: collectorExporter.hostName, - startTimestamp: core.hrTimeToTimeStamp(core.hrTime()), - }, - libraryInfo: { - language: collectorTypes.LibraryInfoLanguage.NODE_JS, - coreLibraryVersion: core.VERSION, - exporterVersion: VERSION, - }, - serviceInfo: { - name: collectorExporter.serviceName, - }, - attributes: collectorExporter.attributes, - }, - resource, - spans, - }; + exporter.traceServiceClient.export( + exportTraceServiceRequest, + ( + err: collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceError + ) => { + if (err) { + collectorExporter.logger.error( + 'exportTraceServiceRequest', + exportTraceServiceRequest + ); + onError(err); + } else { + onSuccess(); + } + } + ); + } else { + exporter.grpcSpansQueue.push({ + spans, + onSuccess, + onError, + }); + } } diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/types.ts b/packages/opentelemetry-exporter-collector/src/platform/node/types.ts new file mode 100644 index 0000000000..8a7786038a --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/node/types.ts @@ -0,0 +1,45 @@ +/*! + * Copyright 2020, OpenTelemetry 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 + * + * https://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 grpc from 'grpc'; +import { ReadableSpan } from '@opentelemetry/tracing'; +import { CollectorExporterError } from '../../types'; + +/** + * Queue item to be used to save temporary spans in case the GRPC service + * hasn't been fully initialised yet + */ +export interface GRPCQueueItem { + spans: ReadableSpan[]; + onSuccess: () => void; + onError: (error: CollectorExporterError) => void; +} + +/** + * Trace Service Client for sending spans + */ +export interface TraceServiceClient extends grpc.Client { + export: (request: any, callback: Function) => {}; +} + +/** + * Interface to store helper information + */ +export interface CollectorData { + traceServiceClient?: TraceServiceClient; + isShutDown: boolean; + grpcSpansQueue: GRPCQueueItem[]; +} diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/util.ts b/packages/opentelemetry-exporter-collector/src/platform/node/util.ts new file mode 100644 index 0000000000..ccc4fb8205 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/node/util.ts @@ -0,0 +1,24 @@ +/*! + * Copyright 2020, OpenTelemetry 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 + * + * https://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. + */ + +/** + * It will remove http or https from the link as grpc requires link without + * protocol + * @param url + */ +export function removeProtocol(url: string): string { + return url.replace(/^https?\:\/\//, ''); +} diff --git a/packages/opentelemetry-exporter-collector/src/transform.ts b/packages/opentelemetry-exporter-collector/src/transform.ts index 84fb97f3d9..580434c990 100644 --- a/packages/opentelemetry-exporter-collector/src/transform.ts +++ b/packages/opentelemetry-exporter-collector/src/transform.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2019, OpenTelemetry Authors + * Copyright 2020, OpenTelemetry Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,221 +14,220 @@ * limitations under the License. */ -import { hexToBase64, hrTimeToTimeStamp } from '@opentelemetry/core'; -import { ReadableSpan } from '@opentelemetry/tracing'; -import { Attributes, Link, TimedEvent, TraceState } from '@opentelemetry/api'; -import * as collectorTypes from './types'; +import { + Attributes, + Link, + SpanKind, + TimedEvent, + TraceState, +} from '@opentelemetry/api'; +import { SDK_INFO } from '@opentelemetry/base'; +import * as core from '@opentelemetry/core'; import { Resource } from '@opentelemetry/resources'; - -const OT_MAX_STRING_LENGTH = 128; - -/** - * convert string to maximum length of 128, providing information of truncated bytes - * @param name - string to be converted - */ -export function toCollectorTruncatableString( - name: string -): collectorTypes.TruncatableString { - const value = name.substr(0, OT_MAX_STRING_LENGTH); - const truncatedByteCount = - name.length > OT_MAX_STRING_LENGTH ? name.length - OT_MAX_STRING_LENGTH : 0; - - return { value, truncatedByteCount }; -} +import { ReadableSpan } from '@opentelemetry/tracing'; +import { CollectorExporter } from './CollectorExporter'; +import { COLLETOR_SPAN_KIND_MAPPING, opentelemetryProto } from './types'; +import ValueType = opentelemetryProto.common.v1.ValueType; /** - * convert attributes + * Converts attributes * @param attributes */ export function toCollectorAttributes( attributes: Attributes -): collectorTypes.Attributes { - const attributeMap: collectorTypes.AttributeMap = {}; - Object.keys(attributes || {}).forEach(key => { - attributeMap[key] = toCollectorEventValue(attributes[key]); +): opentelemetryProto.common.v1.AttributeKeyValue[] { + return Object.keys(attributes).map(key => { + return toCollectorAttributeKeyValue(key, attributes[key]); }); - - return { - droppedAttributesCount: 0, - attributeMap, - }; } /** - * convert event value + * Converts key and value to AttributeKeyValue * @param value event value */ -export function toCollectorEventValue( +export function toCollectorAttributeKeyValue( + key: string, value: unknown -): collectorTypes.AttributeValue { - const attributeValue: collectorTypes.AttributeValue = {}; - +): opentelemetryProto.common.v1.AttributeKeyValue { + let aType: opentelemetryProto.common.v1.ValueType = ValueType.STRING; + const AttributeKeyValue: opentelemetryProto.common.v1.AttributeKeyValue = { + key, + type: 0, + }; if (typeof value === 'string') { - attributeValue.stringValue = toCollectorTruncatableString(value); + AttributeKeyValue.stringValue = value; } else if (typeof value === 'boolean') { - attributeValue.boolValue = value; + aType = ValueType.BOOL; + AttributeKeyValue.boolValue = value; } else if (typeof value === 'number') { // all numbers will be treated as double - attributeValue.doubleValue = value; + aType = ValueType.DOUBLE; + AttributeKeyValue.doubleValue = value; } - return attributeValue; + AttributeKeyValue.type = aType; + + return AttributeKeyValue; } /** - * convert events + * + * Converts events * @param events array of events - * @param maxAttributes - maximum number of event attributes to be converted */ export function toCollectorEvents( - events: TimedEvent[] -): collectorTypes.TimeEvents { - let droppedAnnotationsCount = 0; - let droppedMessageEventsCount = 0; // not counting yet as messageEvent is not implemented - - const timeEvent: collectorTypes.TimeEvent[] = events.map( - (event: TimedEvent) => { - let attributes: collectorTypes.Attributes | undefined; - - if (event && event.attributes) { - attributes = toCollectorAttributes(event.attributes); - droppedAnnotationsCount += attributes.droppedAttributesCount || 0; - } - - let annotation: collectorTypes.Annotation = {}; - if (event.name || attributes) { - annotation = {}; - } - - if (event.name) { - annotation.description = toCollectorTruncatableString(event.name); - } - - if (typeof attributes !== 'undefined') { - annotation.attributes = attributes; - } - - // @TODO convert from event.attributes into appropriate MessageEvent - // const messageEvent: collectorTypes.MessageEvent; - - const timeEvent: collectorTypes.TimeEvent = { - time: hrTimeToTimeStamp(event.time), - // messageEvent, - }; - - if (annotation) { - timeEvent.annotation = annotation; - } - - return timeEvent; - } - ); - - return { - timeEvent, - droppedAnnotationsCount, - droppedMessageEventsCount, - }; -} + timedEvents: TimedEvent[] +): opentelemetryProto.trace.v1.Span.Event[] { + return timedEvents.map(timedEvent => { + const timeUnixNano = core.hrTimeToNanoseconds(timedEvent.time); + const name = timedEvent.name; + const attributes = toCollectorAttributes(timedEvent.attributes || {}); + const droppedAttributesCount = 0; + + const protoEvent: opentelemetryProto.trace.v1.Span.Event = { + timeUnixNano, + name, + attributes, + droppedAttributesCount, + }; -/** - * determines the type of link, only parent link type can be determined now - * @TODO refactor this once such data is directly available from {@link Link} - * @param span - * @param link - */ -export function toCollectorLinkType( - span: ReadableSpan, - link: Link -): collectorTypes.LinkType { - const linkSpanId = link.context.spanId; - const linkTraceId = link.context.traceId; - const spanParentId = span.parentSpanId; - const spanTraceId = span.spanContext.traceId; - - if (linkSpanId === spanParentId && linkTraceId === spanTraceId) { - return collectorTypes.LinkType.PARENT_LINKED_SPAN; - } - return collectorTypes.LinkType.UNSPECIFIED; + return protoEvent; + }); } /** - * converts span links + * Converts links * @param span */ -export function toCollectorLinks(span: ReadableSpan): collectorTypes.Links { - const collectorLinks: collectorTypes.Link[] = span.links.map((link: Link) => { - const collectorLink: collectorTypes.Link = { - traceId: hexToBase64(link.context.traceId), - spanId: hexToBase64(link.context.spanId), - type: toCollectorLinkType(span, link), +export function toCollectorLinks( + span: ReadableSpan +): opentelemetryProto.trace.v1.Span.Link[] { + return span.links.map((link: Link) => { + const protoLink: opentelemetryProto.trace.v1.Span.Link = { + traceId: core.hexToBase64(link.context.traceId), + spanId: core.hexToBase64(link.context.spanId), + attributes: toCollectorAttributes(link.attributes || {}), + droppedAttributesCount: 0, }; - - if (link.attributes) { - collectorLink.attributes = toCollectorAttributes(link.attributes); - } - - return collectorLink; + return protoLink; }); - - return { - link: collectorLinks, - droppedLinksCount: 0, - }; } /** + * Converts span * @param span */ -export function toCollectorSpan(span: ReadableSpan): collectorTypes.Span { +export function toCollectorSpan( + span: ReadableSpan +): opentelemetryProto.trace.v1.Span { return { - traceId: hexToBase64(span.spanContext.traceId), - spanId: hexToBase64(span.spanContext.spanId), + traceId: core.hexToBase64(span.spanContext.traceId), + spanId: core.hexToBase64(span.spanContext.spanId), parentSpanId: span.parentSpanId - ? hexToBase64(span.parentSpanId) + ? core.hexToBase64(span.parentSpanId) : undefined, - tracestate: toCollectorTraceState(span.spanContext.traceState), - name: toCollectorTruncatableString(span.name), - kind: span.kind, - startTime: hrTimeToTimeStamp(span.startTime), - endTime: hrTimeToTimeStamp(span.endTime), + traceState: toCollectorTraceState(span.spanContext.traceState), + name: span.name, + kind: toCollectorKind(span.kind), + startTimeUnixNano: core.hrTimeToNanoseconds(span.startTime), + endTimeUnixNano: core.hrTimeToNanoseconds(span.endTime), attributes: toCollectorAttributes(span.attributes), - // stackTrace: // not implemented - timeEvents: toCollectorEvents(span.events), + droppedAttributesCount: 0, + events: toCollectorEvents(span.events), + droppedEventsCount: 0, status: span.status, - sameProcessAsParentSpan: !!span.parentSpanId, links: toCollectorLinks(span), - // childSpanCount: // not implemented + droppedLinksCount: 0, }; } /** - * converts span resource + * Converts resource * @param resource + * @param additionalAttributes */ export function toCollectorResource( - resource: Resource -): collectorTypes.Resource { - const labels: { [key: string]: string } = {}; - Object.keys(resource.labels).forEach( - name => (labels[name] = String(resource.labels[name])) + resource?: Resource, + additionalAttributes: { [key: string]: any } = {} +): opentelemetryProto.resource.v1.Resource { + const attr = Object.assign( + {}, + additionalAttributes, + resource ? resource.labels : {} ); - // @TODO: add type support - return { labels }; + const resourceProto: opentelemetryProto.resource.v1.Resource = { + attributes: toCollectorAttributes(attr), + droppedAttributesCount: 0, + }; + + return resourceProto; +} + +/** + * Converts span kind + * @param kind + */ +export function toCollectorKind( + kind: SpanKind +): opentelemetryProto.trace.v1.Span.SpanKind { + const collectorKind = COLLETOR_SPAN_KIND_MAPPING[kind]; + return typeof collectorKind === 'number' + ? collectorKind + : opentelemetryProto.trace.v1.Span.SpanKind.SPAN_KIND_UNSPECIFIED; } /** + * Converts traceState * @param traceState */ -function toCollectorTraceState( +export function toCollectorTraceState( traceState?: TraceState -): collectorTypes.TraceState { - if (!traceState) return {}; - const entries = traceState.serialize().split(','); - const apiTraceState: collectorTypes.TraceState = {}; - for (const entry of entries) { - const [key, value] = entry.split('='); - apiTraceState[key] = value; - } - return apiTraceState; +): opentelemetryProto.trace.v1.Span.TraceState | undefined { + if (!traceState) return undefined; + return traceState.serialize(); +} + +/** + * Prepares trace service request to be sent to collector + * @param spans spans + * @param collectorExporter + * @param [name] Instrumentation Library Name + */ +export function toCollectorExportTraceServiceRequest( + spans: ReadableSpan[], + collectorExporter: CollectorExporter, + name: string = '' +): opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest { + const spansToBeSent: opentelemetryProto.trace.v1.Span[] = spans.map(span => + toCollectorSpan(span) + ); + const resource: Resource = + spans.length > 0 ? spans[0].resource : Resource.empty(); + + const additionalAttributes = Object.assign( + {}, + collectorExporter.attributes || {}, + { + 'service.name': collectorExporter.serviceName, + } + ); + const protoResource: opentelemetryProto.resource.v1.Resource = toCollectorResource( + resource, + additionalAttributes + ); + const instrumentationLibrarySpans: opentelemetryProto.trace.v1.InstrumentationLibrarySpans = { + spans: spansToBeSent, + instrumentationLibrary: { + name: name || `${SDK_INFO.NAME} - ${SDK_INFO.LANGUAGE}`, + version: SDK_INFO.VERSION, + }, + }; + const resourceSpan: opentelemetryProto.trace.v1.ResourceSpans = { + resource: protoResource, + instrumentationLibrarySpans: [instrumentationLibrarySpans], + }; + + return { + resourceSpans: [resourceSpan], + }; } diff --git a/packages/opentelemetry-exporter-collector/src/types.ts b/packages/opentelemetry-exporter-collector/src/types.ts index 1960000d33..99eacdd0cf 100644 --- a/packages/opentelemetry-exporter-collector/src/types.ts +++ b/packages/opentelemetry-exporter-collector/src/types.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2019, OpenTelemetry Authors + * Copyright 2020, OpenTelemetry Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,507 +14,170 @@ * limitations under the License. */ -import { SpanKind, Status } from '@opentelemetry/api'; +import { SpanKind } from '@opentelemetry/api'; +import * as api from '@opentelemetry/api'; // header to prevent instrumentation on request export const OT_REQUEST_HEADER = 'x-opentelemetry-outgoing-request'; -/** - * {@link https://github.com/open-telemetry/opentelemetry-proto/blob/master/opentelemetry/proto/agent/common/v1/common.proto#L66} - */ -export const enum LibraryInfoLanguage { - LANGUAGE_UNSPECIFIED = 0, - NODE_JS = 6, - WEB_JS = 10, -} - -export interface AttributeMap { - [key: string]: AttributeValue; -} - -/** - * A text annotation with a set of attributes. - */ -export interface Annotation { - /** - * A user-supplied message describing the event. - */ - description?: TruncatableString; - /** - * A set of attributes on the annotation. - */ - attributes?: Attributes; -} - -/** - * A set of attributes, each with a key and a value. - */ -export interface Attributes { - /** - * \"/instance_id\": \"my-instance\" \"/http/user_agent\": \"\" - * \"/http/server_latency\": 300 \"abc.com/myattribute\": true - */ - attributeMap?: AttributeMap; - /** - * The number of attributes that were discarded. Attributes can be discarded - * because their keys are too long or because there are too many attributes. - * If this value is 0, then no attributes were dropped. - */ - droppedAttributesCount?: number; -} - -/** - * The value of an Attribute. - */ -export interface AttributeValue { - /** - * A string up to 256 bytes long. - */ - stringValue?: TruncatableString; - /** - * A 64-bit signed integer. May be sent to the API as either number or string - * type (string is needed to accurately express some 64-bit ints). - */ - intValue?: string | number; - /** - * A Boolean value represented by `true` or `false`. - */ - boolValue?: boolean; - /** - * A double precision floating point value. - */ - doubleValue?: number; -} - -/** - * Format for an HTTP/JSON request to a grpc-gateway for a trace span exporter. - */ -export interface ExportTraceServiceRequest { - node?: Node; - - /** A list of Spans that belong to the last received Node. */ - spans?: Span[]; - - /** - * The resource for the spans in this message that do not have an explicit - * resource set. - * If unset, the most recently set resource in the RPC stream applies. It is - * valid to never be set within a stream, e.g. when no resource info is known. - */ - resource?: Resource; -} - -/** Information on OpenTelemetry library that produced the spans/metrics. */ -export interface LibraryInfo { - /** Language of OpenTelemetry Library. */ - language?: LibraryInfoLanguage; - - /** Version of collector exporter of Library. */ - exporterVersion?: string; - - /** Version of OpenTelemetry Library. */ - coreLibraryVersion?: string; -} - -/** - * A pointer from the current span to another span in the same trace or in a - * different trace. For example, this can be used in batching operations, where - * a single batch handler processes multiple requests from different traces or - * when the handler receives a request from a different project. - */ -export interface Link { - /** - * A unique identifier for a trace. All spans from the same trace share the - * same `trace_id`. The ID is a 16-byte array. - */ - traceId?: string; - /** - * A unique identifier for a span within a trace, assigned when the span is - * created. The ID is an 8-byte array. - */ - spanId?: string; - /** - * The relationship of the current span relative to the linked span. - */ - type?: LinkType; - /** - * A set of attributes on the link. - */ - attributes?: Attributes; -} - -/** - * A collection of links, which are references from this span to a span in the - * same or different trace. - */ -export interface Links { - /** - * A collection of links. - */ - link?: Link[]; - /** - * The number of dropped links after the maximum size was enforced. If this - * value is 0, then no links were dropped. - */ - droppedLinksCount?: number; -} - -/** - * The relationship of the current span relative to the linked span: child, - * parent, or unspecified. - */ -export const enum LinkType { - /** - * The relationship of the two spans is unknown, or known but other than - * parent-child. - */ - UNSPECIFIED, - /** The linked span is a child of the current span. */ - CHILD_LINKED_SPAN, - /** The linked span is a parent of the current span. */ - PARENT_LINKED_SPAN, -} - -/** - * An event describing a message sent/received between Spans. - */ -export interface MessageEvent { - /** - * The type of MessageEvent. Indicates whether the message was sent or - * received. - */ - type?: MessageEventType; - /** - * An identifier for the MessageEvent's message that can be used to match SENT - * and RECEIVED MessageEvents. For example, this field could represent a - * sequence ID for a streaming RPC. It is recommended to be unique within a - * Span. - */ - id?: string | number; - /** - * The number of uncompressed bytes sent or received. - */ - uncompressedSize?: string | number; - /** - * The number of compressed bytes sent or received. If zero, assumed to be the - * same size as uncompressed. - */ - compressedSize?: string | number; -} - -/** Indicates whether the message was sent or received. */ -export const enum MessageEventType { - /** Unknown message event type. */ - MESSAGE_EVENT_TYPE_UNSPECIFIED, - /** Indicates a sent message. */ - MESSAGE_EVENT_TYPE_SENT, - /** Indicates a received message. */ - MESSAGE_EVENT_TYPE_RECEIVED, -} - -/** - * A description of a binary module. - */ -export interface Module { - /** - * TODO: document the meaning of this field. For example: main binary, kernel - * modules, and dynamic libraries such as libc.so, sharedlib.so. - */ - module?: TruncatableString; - /** - * A unique identifier for the module, usually a hash of its contents. - */ - buildId?: TruncatableString; -} - -/** - * Identifier metadata of the Node (Application instrumented with OpenTelemetry) - * that connects to OpenTelemetry Agent. - * In the future we plan to extend the identifier proto definition to support - * additional information (e.g cloud id, etc.) - */ -export interface Node { - /** Identifier that uniquely identifies a process within a VM/container. */ - identifier?: ProcessIdentifier; - - /** Information on the OpenTelemetry Library that initiates the stream. */ - libraryInfo?: LibraryInfo; - - /** Additional information on service. */ - serviceInfo?: ServiceInfo; - - /** Additional attributes. */ - attributes?: { [key: string]: unknown }; -} - -/** - * Identifier that uniquely identifies a process within a VM/container. - * For OpenTelemetry Web, this identifies the domain name of the site. - */ -export interface ProcessIdentifier { - /** - * The host name. Usually refers to the machine/container name. - * For example: os.Hostname() in Go, socket.gethostname() in Python. - * This will be the value of `window.location.host` for OpenTelemetry Web. - */ - hostName?: string; - - /** Process id. Not used in OpenTelemetry Web. */ - pid?: number; - - /** Start time of this ProcessIdentifier. Represented in epoch time.. */ - startTimestamp?: string; -} - -/** Resource information. */ -export interface Resource { - /** Type identifier for the resource. */ - type?: string; - - /** Set of labels that describe the resource. */ - labels?: { [key: string]: string }; -} - -/** Additional service information. */ -export interface ServiceInfo { - /** Name of the service. */ - name?: string; -} - -/** - * A span represents a single operation within a trace. Spans can be nested to - * form a trace tree. Often, a trace contains a root span that describes the - * end-to-end latency, and one or more subspans for its sub-operations. A trace - * can also contain multiple root spans, or none at all. Spans do not need to be - * contiguous - there may be gaps or overlaps between spans in a trace. The - * next id is 16. - */ -export interface Span { - /** - * A unique identifier for a trace. All spans from the same trace share the - * same `trace_id`. The ID is a 16-byte array. This field is required. - */ - traceId: string; - /** - * A unique identifier for a span within a trace, assigned when the span is - * created. The ID is an 8-byte array. This field is required. - */ - spanId: string; - /** - * The `tracestate` field conveys information about request position in - * multiple distributed tracing graphs. There can be a maximum of 32 members - * in the map. The key must begin with a lowercase letter, and can only - * contain lowercase letters 'a'-'z', digits '0'-'9', underscores '_', dashes - * '-', asterisks '*', and forward slashes '/'. For multi-tenant vendors - * scenarios '@' sign can be used to prefix vendor name. The maximum length - * for the key is 256 characters. The value is opaque string up to 256 - * characters printable ASCII RFC0020 characters (i.e., the range 0x20 to - * 0x7E) except ',' and '='. Note that this also excludes tabs, newlines, - * carriage returns, etc. See the https://github.com/w3c/distributed-tracing - * for more details about this field. - */ - tracestate?: TraceState; - /** - * The `span_id` of this span's parent span. If this is a root span, then this - * field must be empty. The ID is an 8-byte array. - */ - parentSpanId?: string; - /** - * A description of the span's operation. For example, the name can be a - * qualified method name or a file name and a line number where the operation - * is called. A best practice is to use the same display name at the same call - * point in an application. This makes it easier to correlate spans in - * different traces. This field is required. - */ - name?: TruncatableString; - /** - * Distinguishes between spans generated in a particular context. For example, - * two spans with the same name may be distinguished using `CLIENT` and - * `SERVER` to identify queueing latency associated with the span. - */ - kind?: SpanKind; - /** - * The start time of the span. On the client side, this is the time kept by - * the local machine where the span execution starts. On the server side, this - * is the time when the server's application handler starts running. - * the format should be a timestamp for example '2019-11-15T18:59:36.489343982Z' - */ - startTime?: string; - /** - * The end time of the span. On the client side, this is the time kept by the - * local machine where the span execution ends. On the server side, this is - * the time when the server application handler stops running. - * the format should be a timestamp for example '2019-11-15T18:59:36.489343982Z' - */ - endTime?: string; - /** - * A set of attributes on the span. - */ - attributes?: Attributes; - /** - * A stack trace captured at the start of the span. - * Currently not used - */ - stackTrace?: StackTrace; - /** - * The included time events. - */ - timeEvents?: TimeEvents; - /** - * An optional final status for this span. - */ - status?: Status; - /** - * A highly recommended but not required flag that identifies when a trace - * crosses a process boundary. True when the parent_span belongs to the same - * process as the current span. - */ - sameProcessAsParentSpan?: boolean; - - //@TODO - do we use it in opentelemetry or it is not needed? - // /** - // * An optional number of child spans that were generated while this span was - // * active. If set, allows an implementation to detect missing child spans. - // */ - // childSpanCount?: number; - /** - * The included links. - */ - links?: Links; -} - -/** - * A single stack frame in a stack trace. - */ -export interface StackFrame { - /** - * The fully-qualified name that uniquely identifies the function or method - * that is active in this frame. - */ - functionName?: TruncatableString; - /** - * An un-mangled function name, if `function_name` is - * [mangled](http://www.avabodh.com/cxxin/namemangling.html). The name can be - * fully qualified. - */ - originalFunctionName?: TruncatableString; - /** - * The name of the source file where the function call appears. - */ - fileName?: TruncatableString; - /** - * The line number in `file_name` where the function call appears. - */ - lineNumber?: string; - /** - * The column number where the function call appears, if available. This is - * important in JavaScript because of its anonymous functions. - */ - columnNumber?: string; - /** - * The binary module from where the code was loaded. - */ - loadModule?: Module; - /** - * The version of the deployed source code. - */ - sourceVersion?: TruncatableString; -} - -/** - * A collection of stack frames, which can be truncated. - */ -export interface StackFrames { - /** - * Stack frames in this call stack. - */ - frame?: StackFrame[]; - /** - * The number of stack frames that were dropped because there were too many - * stack frames. If this value is 0, then no stack frames were dropped. - */ - droppedFramesCount?: number; -} - -/** - * The call stack which originated this span. - */ -export interface StackTrace { - /** - * Stack frames in this stack trace. - */ - stackFrames?: StackFrames; - /** - * The hash ID is used to conserve network bandwidth for duplicate stack - * traces within a single trace. Often multiple spans will have identical - * stack traces. The first occurrence of a stack trace should contain both - * `stack_frames` and a value in `stack_trace_hash_id`. Subsequent spans - * within the same request can refer to that stack trace by setting only - * `stack_trace_hash_id`. - */ - stackTraceHashId?: string; -} - -/** - * A time-stamped annotation or message event in the Span. - */ -export interface TimeEvent { - /** - * The time the event occurred. - */ - time?: string; - /** - * A text annotation with a set of attributes. - */ - annotation?: Annotation; - /** - * An event describing a message sent/received between Spans. - */ - messageEvent?: MessageEvent; -} - -/** - * A collection of `TimeEvent`s. A `TimeEvent` is a time-stamped annotation on - * the span, consisting of either user-supplied key-value pairs, or details of a - * message sent/received between Spans. - */ -export interface TimeEvents { - /** - * A collection of `TimeEvent`s. - */ - timeEvent?: TimeEvent[]; - /** - * The number of dropped annotations in all the included time events. If the - * value is 0, then no annotations were dropped. - */ - droppedAnnotationsCount?: number; - /** - * The number of dropped message events in all the included time events. If - * the value is 0, then no message events were dropped. - */ - droppedMessageEventsCount?: number; -} - -/** - * A string that might be shortened to a specified length. - */ -export interface TruncatableString { - /** - * The shortened string. For example, if the original string was 500 bytes - * long and the limit of the string was 128 bytes, then this value contains - * the first 128 bytes of the 500-byte string. Note that truncation always - * happens on a character boundary, to ensure that a truncated string is still - * valid UTF-8. Because it may contain multi-byte characters, the size of the - * truncated string may be less than the truncation limit. - */ - value?: string; - /** - * The number of bytes removed from the original string. If this value is 0, - * then the string was not shortened. - */ - truncatedByteCount?: number; -} - -export interface TraceState { - [key: string]: string; -} +export namespace opentelemetryProto { + export namespace collector { + export namespace trace.v1 { + export interface TraceService { + service: opentelemetryProto.collector.trace.v1.TraceService; + } + + export interface ExportTraceServiceRequest { + resourceSpans: opentelemetryProto.trace.v1.ResourceSpans[]; + } + + export interface ExportTraceServiceResponse {} + + export interface ExportTraceServiceError { + code: number; + details: string; + metadata: { [key: string]: unknown }; + message: string; + stack: string; + } + } + } + + export namespace resource.v1 { + export interface Resource { + attributes: opentelemetryProto.common.v1.AttributeKeyValue[]; + droppedAttributesCount: number; + } + } + + export namespace trace.v1 { + export namespace ConstantSampler { + export enum ConstantDecision { + ALWAYS_OFF = 0, + ALWAYS_ON = 1, + ALWAYS_PARENT = 2, + } + } + export namespace Span { + export interface Event { + timeUnixNano: number; + name: string; + attributes?: opentelemetryProto.common.v1.AttributeKeyValue[]; + droppedAttributesCount: number; + } + + export interface Link { + traceId: string; + spanId: string; + traceState?: opentelemetryProto.trace.v1.Span.TraceState; + attributes?: opentelemetryProto.common.v1.AttributeKeyValue[]; + droppedAttributesCount: number; + } + + export enum SpanKind { + SPAN_KIND_UNSPECIFIED, + INTERNAL, + SERVER, + CLIENT, + PRODUCER, + CONSUMER, + } + export type TraceState = string | undefined; + } + + export interface ConstantSampler { + decision?: opentelemetryProto.trace.v1.ConstantSampler.ConstantDecision; + } + + export interface InstrumentationLibrarySpans { + instrumentationLibrary?: opentelemetryProto.common.v1.InstrumentationLibrary; + spans: opentelemetryProto.trace.v1.Span[]; + } + + export interface ProbabilitySampler { + samplingProbability?: number | null; + } + + export interface RateLimitingSampler { + qps?: number | null; + } + + export interface ResourceSpans { + resource?: opentelemetryProto.resource.v1.Resource; + instrumentationLibrarySpans: opentelemetryProto.trace.v1.InstrumentationLibrarySpans[]; + } + + export interface Span { + traceId: string; + spanId: string; + traceState: opentelemetryProto.trace.v1.Span.TraceState; + parentSpanId?: string; + name?: string; + kind?: opentelemetryProto.trace.v1.Span.SpanKind; + startTimeUnixNano?: number; + endTimeUnixNano?: number; + attributes?: opentelemetryProto.common.v1.AttributeKeyValue[]; + droppedAttributesCount: number; + events?: opentelemetryProto.trace.v1.Span.Event[]; + droppedEventsCount: number; + links?: opentelemetryProto.trace.v1.Span.Link[]; + droppedLinksCount: number; + status?: Status; + } + + export interface Status extends api.Status {} + + export interface TraceConfig { + constantSampler?: ConstantSampler | null; + probabilitySampler?: ProbabilitySampler | null; + rateLimitingSampler?: RateLimitingSampler | null; + } + } + export namespace common.v1 { + export interface AttributeKeyValue { + key: string; + type: opentelemetryProto.common.v1.ValueType; + stringValue?: string; + intValue?: number; + doubleValue?: number; + boolValue?: boolean; + } + + export interface InstrumentationLibrary { + name: string; + version: string; + } + + export interface StringKeyValue { + key: string; + value: string; + } + + export enum ValueType { + STRING, + INT, + DOUBLE, + BOOL, + } + } +} + +/** + * Interface for handling error + */ +export interface CollectorExporterError { + code?: number; + message?: string; + stack?: string; +} + +/** + * Mapping between api SpanKind and proto SpanKind + */ +export const COLLETOR_SPAN_KIND_MAPPING = { + [SpanKind.INTERNAL]: opentelemetryProto.trace.v1.Span.SpanKind.INTERNAL, + [SpanKind.SERVER]: opentelemetryProto.trace.v1.Span.SpanKind.SERVER, + [SpanKind.CLIENT]: opentelemetryProto.trace.v1.Span.SpanKind.CLIENT, + [SpanKind.PRODUCER]: opentelemetryProto.trace.v1.Span.SpanKind.PRODUCER, + [SpanKind.CONSUMER]: opentelemetryProto.trace.v1.Span.SpanKind.CONSUMER, +}; diff --git a/packages/opentelemetry-exporter-collector/test/browser/CollectorExporter.test.ts b/packages/opentelemetry-exporter-collector/test/browser/CollectorExporter.test.ts index 0a11981256..d17551c866 100644 --- a/packages/opentelemetry-exporter-collector/test/browser/CollectorExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/browser/CollectorExporter.test.ts @@ -25,8 +25,9 @@ import { import * as collectorTypes from '../../src/types'; import { - ensureExportTraceServiceRequestIsSet, ensureSpanIsCorrect, + ensureExportTraceServiceRequestIsSet, + ensureWebResourceIsCorrect, mockedReadableSpan, } from '../helper'; const sendBeacon = navigator.sendBeacon; @@ -73,19 +74,27 @@ describe('CollectorExporter - web', () => { const body = args[1]; const json = JSON.parse( body - ) as collectorTypes.ExportTraceServiceRequest; - const span1 = json.spans && json.spans[0]; + ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + const span1 = + json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); if (span1) { ensureSpanIsCorrect(span1); } + + const resource = json.resourceSpans[0].resource; + assert.ok(typeof resource !== 'undefined', "resource doesn't exist"); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + assert.strictEqual(url, 'http://foo.bar.com'); assert.strictEqual(spyBeacon.callCount, 1); assert.strictEqual(spyOpen.callCount, 0); - ensureExportTraceServiceRequestIsSet(json, 10); + ensureExportTraceServiceRequestIsSet(json); done(); }); @@ -148,16 +157,24 @@ describe('CollectorExporter - web', () => { const body = request.requestBody; const json = JSON.parse( body - ) as collectorTypes.ExportTraceServiceRequest; - const span1 = json.spans && json.spans[0]; + ) as collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + const span1 = + json.resourceSpans[0].instrumentationLibrarySpans[0].spans[0]; assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); if (span1) { ensureSpanIsCorrect(span1); } + + const resource = json.resourceSpans[0].resource; + assert.ok(typeof resource !== 'undefined', "resource doesn't exist"); + if (resource) { + ensureWebResourceIsCorrect(resource); + } + assert.strictEqual(spyBeacon.callCount, 0); - ensureExportTraceServiceRequestIsSet(json, 10); + ensureExportTraceServiceRequestIsSet(json); done(); }); @@ -191,8 +208,10 @@ describe('CollectorExporter - web', () => { const request = server.requests[0]; request.respond(400); - const response: any = spyLoggerError.args[0][0]; - assert.strictEqual(response, 'xhr error'); + const response1: any = spyLoggerError.args[0][0]; + const response2: any = spyLoggerError.args[1][0]; + assert.strictEqual(response1, 'body'); + assert.strictEqual(response2, 'xhr error'); assert.strictEqual(spyBeacon.callCount, 0); done(); diff --git a/packages/opentelemetry-exporter-collector/test/common/CollectorExporter.test.ts b/packages/opentelemetry-exporter-collector/test/common/CollectorExporter.test.ts index 1c37f37b8e..175222f0e4 100644 --- a/packages/opentelemetry-exporter-collector/test/common/CollectorExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/common/CollectorExporter.test.ts @@ -23,10 +23,9 @@ import { CollectorExporter, CollectorExporterConfig, } from '../../src/CollectorExporter'; -import * as collectorTypes from '../../src/types'; import * as platform from '../../src/platform/index'; -import { ensureSpanIsCorrect, mockedReadableSpan } from '../helper'; +import { mockedReadableSpan } from '../helper'; describe('CollectorExporter - common', () => { let collectorExporter: CollectorExporter; @@ -55,7 +54,7 @@ describe('CollectorExporter - common', () => { it('should call onInit', () => { assert.strictEqual(onInitSpy.callCount, 1); - assert.ok(onInitSpy.args[0][0] === collectorExporter.shutdown); + assert.ok(onInitSpy.args[0][0] === collectorExporter); }); describe('when config contains certain params', () => { @@ -107,8 +106,8 @@ describe('CollectorExporter - common', () => { collectorExporter.export(spans, function() {}); setTimeout(() => { - const span1 = spySend.args[0][0][0] as collectorTypes.Span; - ensureSpanIsCorrect(span1); + const span1 = spySend.args[0][0][0] as ReadableSpan; + assert.deepStrictEqual(spans[0], span1); done(); }); assert.strictEqual(spySend.callCount, 1); @@ -155,7 +154,7 @@ describe('CollectorExporter - common', () => { it('should call onShutdown', done => { collectorExporter.shutdown(); setTimeout(() => { - assert.ok(onShutdownSpy.args[0][0] === collectorExporter.shutdown); + assert.ok(onShutdownSpy.args[0][0] === collectorExporter); done(); }); }); diff --git a/packages/opentelemetry-exporter-collector/test/common/transform.test.ts b/packages/opentelemetry-exporter-collector/test/common/transform.test.ts index 544c7a136f..266b83aa5c 100644 --- a/packages/opentelemetry-exporter-collector/test/common/transform.test.ts +++ b/packages/opentelemetry-exporter-collector/test/common/transform.test.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2019, OpenTelemetry Authors + * Copyright 2020, OpenTelemetry Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,84 +21,41 @@ import { ensureSpanIsCorrect, mockedReadableSpan } from '../helper'; import { Resource } from '@opentelemetry/resources'; describe('transform', () => { - describe('toCollectorTruncatableString', () => { - it('should convert string to TruncatableString', () => { - assert.deepStrictEqual(transform.toCollectorTruncatableString('foo'), { - truncatedByteCount: 0, - value: 'foo', - }); - }); - - it('should convert long string to TruncatableString', () => { - let foo = - 'foo1234567890foo1234567890foo1234567890foo1234567890foo1234567890foo1234567890foo1234567890'; - foo += foo; - assert.deepStrictEqual(transform.toCollectorTruncatableString(foo), { - truncatedByteCount: 54, - value: - 'foo1234567890foo1234567890foo1234567890foo1234567890foo1234567890foo1234567890foo1234567890foo1234567890foo1234567890foo12345678', - }); - }); - }); - describe('toCollectorAttributes', () => { it('should convert attribute string', () => { const attributes: Attributes = { foo: 'bar', }; - assert.deepStrictEqual(transform.toCollectorAttributes(attributes), { - attributeMap: { - foo: { - stringValue: { - truncatedByteCount: 0, - value: 'bar', - }, - }, - }, - droppedAttributesCount: 0, - }); + assert.deepStrictEqual(transform.toCollectorAttributes(attributes), [ + { key: 'foo', type: 0, stringValue: 'bar' }, + ]); }); it('should convert attribute integer', () => { const attributes: Attributes = { foo: 13, }; - assert.deepStrictEqual(transform.toCollectorAttributes(attributes), { - attributeMap: { - foo: { - doubleValue: 13, - }, - }, - droppedAttributesCount: 0, - }); + assert.deepStrictEqual(transform.toCollectorAttributes(attributes), [ + { key: 'foo', type: 2, doubleValue: 13 }, + ]); }); it('should convert attribute boolean', () => { const attributes: Attributes = { foo: true, }; - assert.deepStrictEqual(transform.toCollectorAttributes(attributes), { - attributeMap: { - foo: { - boolValue: true, - }, - }, - droppedAttributesCount: 0, - }); + assert.deepStrictEqual(transform.toCollectorAttributes(attributes), [ + { key: 'foo', type: 3, boolValue: true }, + ]); }); it('should convert attribute double', () => { const attributes: Attributes = { foo: 1.34, }; - assert.deepStrictEqual(transform.toCollectorAttributes(attributes), { - attributeMap: { - foo: { - doubleValue: 1.34, - }, - }, - droppedAttributesCount: 0, - }); + assert.deepStrictEqual(transform.toCollectorAttributes(attributes), [ + { key: 'foo', type: 2, doubleValue: 1.34 }, + ]); }); }); @@ -112,36 +69,20 @@ describe('transform', () => { attributes: { c: 'd' }, }, ]; - assert.deepStrictEqual(transform.toCollectorEvents(events), { - timeEvent: [ - { - time: '1970-01-01T00:02:03.000000123Z', - annotation: { - description: { value: 'foo', truncatedByteCount: 0 }, - attributes: { - droppedAttributesCount: 0, - attributeMap: { - a: { stringValue: { value: 'b', truncatedByteCount: 0 } }, - }, - }, - }, - }, - { - time: '1970-01-01T00:05:21.000000321Z', - annotation: { - description: { value: 'foo2', truncatedByteCount: 0 }, - attributes: { - droppedAttributesCount: 0, - attributeMap: { - c: { stringValue: { value: 'd', truncatedByteCount: 0 } }, - }, - }, - }, - }, - ], - droppedAnnotationsCount: 0, - droppedMessageEventsCount: 0, - }); + assert.deepStrictEqual(transform.toCollectorEvents(events), [ + { + timeUnixNano: 123000000123, + name: 'foo', + attributes: [{ key: 'a', type: 0, stringValue: 'b' }], + droppedAttributesCount: 0, + }, + { + timeUnixNano: 321000000321, + name: 'foo2', + attributes: [{ key: 'c', type: 0, stringValue: 'd' }], + droppedAttributesCount: 0, + }, + ]); }); }); @@ -161,11 +102,20 @@ describe('transform', () => { }) ); assert.deepStrictEqual(resource, { - labels: { - service: 'ui', - version: '1', - success: 'true', - }, + attributes: [ + { + key: 'service', + type: 0, + stringValue: 'ui', + }, + { + key: 'version', + type: 2, + doubleValue: 1, + }, + { key: 'success', type: 3, boolValue: true }, + ], + droppedAttributesCount: 0, }); }); }); diff --git a/packages/opentelemetry-exporter-collector/test/helper.ts b/packages/opentelemetry-exporter-collector/test/helper.ts index 043865b86a..29302aa6ad 100644 --- a/packages/opentelemetry-exporter-collector/test/helper.ts +++ b/packages/opentelemetry-exporter-collector/test/helper.ts @@ -1,5 +1,5 @@ /*! - * Copyright 2019, OpenTelemetry Authors + * Copyright 2020, OpenTelemetry Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,13 +15,45 @@ */ import { TraceFlags } from '@opentelemetry/api'; -import * as core from '@opentelemetry/core'; import { ReadableSpan } from '@opentelemetry/tracing'; import { Resource } from '@opentelemetry/resources'; import * as assert from 'assert'; -import * as transform from '../src/transform'; +import { opentelemetryProto } from '../src/types'; import * as collectorTypes from '../src/types'; -import { VERSION } from '../src/version'; + +if (typeof Buffer === 'undefined') { + // @ts-ignore + window.Buffer = { + from: function(arr: []) { + return new Uint8Array(arr); + }, + }; +} + +const traceIdArr = [ + 31, + 16, + 8, + 220, + 142, + 39, + 14, + 133, + 196, + 10, + 13, + 124, + 57, + 57, + 178, + 120, +]; +const spanIdArr = [94, 16, 114, 97, 246, 79, 165, 62]; +const parentIdArr = [120, 168, 145, 80, 152, 134, 67, 136]; + +const traceIdBase64 = 'HxAI3I4nDoXECg18OTmyeA=='; +const spanIdBase64 = 'XhByYfZPpT4='; +const parentIdBase64 = 'eKiRUJiGQ4g='; export const mockedReadableSpan: ReadableSpan = { name: 'documentFetch', @@ -76,127 +108,405 @@ export const mockedReadableSpan: ReadableSpan = { }), }; -export function ensureSpanIsCorrect(span: collectorTypes.Span) { - assert.deepStrictEqual(transform.toCollectorSpan(mockedReadableSpan), { - traceId: 'HxAI3I4nDoXECg18OTmyeA==', - spanId: 'XhByYfZPpT4=', - parentSpanId: 'eKiRUJiGQ4g=', - tracestate: {}, - name: { value: 'documentFetch', truncatedByteCount: 0 }, - kind: 0, - startTime: '2019-11-18T23:36:05.429803070Z', - endTime: '2019-11-18T23:36:05.438688070Z', - attributes: { - droppedAttributesCount: 0, - attributeMap: { - component: { - stringValue: { value: 'document-load', truncatedByteCount: 0 }, - }, +export function ensureExportedEventsAreCorrect( + events: opentelemetryProto.trace.v1.Span.Event[] +) { + assert.deepStrictEqual( + events, + [ + { + attributes: [], + timeUnixNano: '1574120165429803008', + name: 'fetchStart', + droppedAttributesCount: 0, }, - }, - timeEvents: { - timeEvent: [ - { - time: '2019-11-18T23:36:05.429803070Z', - annotation: { - description: { value: 'fetchStart', truncatedByteCount: 0 }, - }, - }, - { - time: '2019-11-18T23:36:05.429803070Z', - annotation: { - description: { - value: 'domainLookupStart', - truncatedByteCount: 0, - }, - }, - }, - { - time: '2019-11-18T23:36:05.429803070Z', - annotation: { - description: { - value: 'domainLookupEnd', - truncatedByteCount: 0, - }, - }, - }, - { - time: '2019-11-18T23:36:05.429803070Z', - annotation: { - description: { value: 'connectStart', truncatedByteCount: 0 }, - }, - }, - { - time: '2019-11-18T23:36:05.429803070Z', - annotation: { - description: { value: 'connectEnd', truncatedByteCount: 0 }, - }, - }, - { - time: '2019-11-18T23:36:05.435513070Z', - annotation: { - description: { value: 'requestStart', truncatedByteCount: 0 }, - }, - }, - { - time: '2019-11-18T23:36:05.436923070Z', - annotation: { - description: { value: 'responseStart', truncatedByteCount: 0 }, - }, - }, - { - time: '2019-11-18T23:36:05.438688070Z', - annotation: { - description: { value: 'responseEnd', truncatedByteCount: 0 }, + { + attributes: [], + timeUnixNano: '1574120165429803008', + name: 'domainLookupStart', + droppedAttributesCount: 0, + }, + { + attributes: [], + timeUnixNano: '1574120165429803008', + name: 'domainLookupEnd', + droppedAttributesCount: 0, + }, + { + attributes: [], + timeUnixNano: '1574120165429803008', + name: 'connectStart', + droppedAttributesCount: 0, + }, + { + attributes: [], + timeUnixNano: '1574120165429803008', + name: 'connectEnd', + droppedAttributesCount: 0, + }, + { + attributes: [], + timeUnixNano: '1574120165435513088', + name: 'requestStart', + droppedAttributesCount: 0, + }, + { + attributes: [], + timeUnixNano: '1574120165436923136', + name: 'responseStart', + droppedAttributesCount: 0, + }, + { + attributes: [], + timeUnixNano: '1574120165438688000', + name: 'responseEnd', + droppedAttributesCount: 0, + }, + ], + 'exported events are incorrect' + ); +} + +export function ensureExportedAttributesAreCorrect( + attributes: opentelemetryProto.common.v1.AttributeKeyValue[] +) { + assert.deepStrictEqual( + attributes, + [ + { + key: 'component', + type: 'STRING', + stringValue: 'document-load', + intValue: '0', + doubleValue: 0, + boolValue: false, + }, + ], + 'exported attributes are incorrect' + ); +} + +export function ensureExportedLinksAreCorrect( + attributes: opentelemetryProto.trace.v1.Span.Link[] +) { + assert.deepStrictEqual( + attributes, + [ + { + attributes: [ + { + key: 'component', + type: 'STRING', + stringValue: 'document-load', + intValue: '0', + doubleValue: 0, + boolValue: false, }, - }, - ], - droppedAnnotationsCount: 0, - droppedMessageEventsCount: 0, - }, - status: { code: 0 }, - sameProcessAsParentSpan: true, - links: { - droppedLinksCount: 0, - link: [ - { - traceId: 'HxAI3I4nDoXECg18OTmyeA==', - spanId: 'eKiRUJiGQ4g=', - type: 2, - attributes: { - droppedAttributesCount: 0, - attributeMap: { - component: { - stringValue: { value: 'document-load', truncatedByteCount: 0 }, - }, - }, + ], + traceId: Buffer.from(traceIdArr), + spanId: Buffer.from(parentIdArr), + traceState: '', + droppedAttributesCount: 0, + }, + ], + 'exported links are incorrect' + ); +} + +export function ensureEventsAreCorrect( + events: opentelemetryProto.trace.v1.Span.Event[] +) { + assert.deepStrictEqual( + events, + [ + { + timeUnixNano: 1574120165429803000, + name: 'fetchStart', + attributes: [], + droppedAttributesCount: 0, + }, + { + timeUnixNano: 1574120165429803000, + name: 'domainLookupStart', + attributes: [], + droppedAttributesCount: 0, + }, + { + timeUnixNano: 1574120165429803000, + name: 'domainLookupEnd', + attributes: [], + droppedAttributesCount: 0, + }, + { + timeUnixNano: 1574120165429803000, + name: 'connectStart', + attributes: [], + droppedAttributesCount: 0, + }, + { + timeUnixNano: 1574120165429803000, + name: 'connectEnd', + attributes: [], + droppedAttributesCount: 0, + }, + { + timeUnixNano: 1574120165435513000, + name: 'requestStart', + attributes: [], + droppedAttributesCount: 0, + }, + { + timeUnixNano: 1574120165436923100, + name: 'responseStart', + attributes: [], + droppedAttributesCount: 0, + }, + { + timeUnixNano: 1574120165438688000, + name: 'responseEnd', + attributes: [], + droppedAttributesCount: 0, + }, + ], + 'events are incorrect' + ); +} + +export function ensureAttributesAreCorrect( + attributes: opentelemetryProto.common.v1.AttributeKeyValue[] +) { + assert.deepStrictEqual( + attributes, + [ + { + key: 'component', + type: 0, + stringValue: 'document-load', + }, + ], + 'attributes are incorrect' + ); +} + +export function ensureLinksAreCorrect( + attributes: opentelemetryProto.trace.v1.Span.Link[] +) { + assert.deepStrictEqual( + attributes, + [ + { + traceId: traceIdBase64, + spanId: parentIdBase64, + attributes: [ + { + key: 'component', + type: 0, + stringValue: 'document-load', }, - }, - ], - }, + ], + droppedAttributesCount: 0, + }, + ], + 'links are incorrect' + ); +} + +export function ensureSpanIsCorrect( + span: collectorTypes.opentelemetryProto.trace.v1.Span +) { + if (span.attributes) { + ensureAttributesAreCorrect(span.attributes); + } + if (span.events) { + ensureEventsAreCorrect(span.events); + } + if (span.links) { + ensureLinksAreCorrect(span.links); + } + assert.deepStrictEqual(span.traceId, traceIdBase64, 'traceId is wrong'); + assert.deepStrictEqual(span.spanId, spanIdBase64, 'spanId is wrong'); + assert.deepStrictEqual( + span.parentSpanId, + parentIdBase64, + 'parentIdArr is wrong' + ); + assert.strictEqual(span.name, 'documentFetch', 'name is wrong'); + assert.strictEqual( + span.kind, + opentelemetryProto.trace.v1.Span.SpanKind.INTERNAL, + 'kind is wrong' + ); + assert.strictEqual( + span.startTimeUnixNano, + 1574120165429803008, + 'startTimeUnixNano is wrong' + ); + assert.strictEqual( + span.endTimeUnixNano, + 1574120165438688000, + 'endTimeUnixNano is wrong' + ); + assert.strictEqual( + span.droppedAttributesCount, + 0, + 'droppedAttributesCount is wrong' + ); + assert.strictEqual(span.droppedEventsCount, 0, 'droppedEventsCount is wrong'); + assert.strictEqual(span.droppedLinksCount, 0, 'droppedLinksCount is wrong'); + assert.deepStrictEqual(span.status, { code: 0 }, 'status is wrong'); +} + +export function ensureExportedSpanIsCorrect( + span: collectorTypes.opentelemetryProto.trace.v1.Span +) { + if (span.attributes) { + ensureExportedAttributesAreCorrect(span.attributes); + } + if (span.events) { + ensureExportedEventsAreCorrect(span.events); + } + if (span.links) { + ensureExportedLinksAreCorrect(span.links); + } + assert.deepStrictEqual( + span.traceId, + Buffer.from(traceIdArr), + 'traceId is wrong' + ); + assert.deepStrictEqual( + span.spanId, + Buffer.from(spanIdArr), + 'spanId is wrong' + ); + assert.strictEqual(span.traceState, '', 'traceState is wrong'); + assert.deepStrictEqual( + span.parentSpanId, + Buffer.from(parentIdArr), + 'parentIdArr is wrong' + ); + assert.strictEqual(span.name, 'documentFetch', 'name is wrong'); + assert.strictEqual(span.kind, 'INTERNAL', 'kind is wrong'); + assert.strictEqual( + span.startTimeUnixNano, + '1574120165429803008', + 'startTimeUnixNano is wrong' + ); + assert.strictEqual( + span.endTimeUnixNano, + '1574120165438688000', + 'endTimeUnixNano is wrong' + ); + assert.strictEqual( + span.droppedAttributesCount, + 0, + 'droppedAttributesCount is wrong' + ); + assert.strictEqual(span.droppedEventsCount, 0, 'droppedEventsCount is wrong'); + assert.strictEqual(span.droppedLinksCount, 0, 'droppedLinksCount is wrong'); + assert.deepStrictEqual( + span.status, + { code: 'Ok', message: '' }, + 'status is wrong' + ); +} + +export function ensureWebResourceIsCorrect( + resource: collectorTypes.opentelemetryProto.resource.v1.Resource +) { + assert.deepStrictEqual(resource, { + attributes: [ + { + key: 'service.name', + type: 0, + stringValue: 'bar', + }, + { + key: 'service', + type: 0, + stringValue: 'ui', + }, + { key: 'version', type: 2, doubleValue: 1 }, + { + key: 'cost', + type: 2, + doubleValue: 112.12, + }, + ], + droppedAttributesCount: 0, }); } -export function ensureExportTraceServiceRequestIsSet( - json: collectorTypes.ExportTraceServiceRequest, - languageInfo: collectorTypes.LibraryInfoLanguage +export function ensureResourceIsCorrect( + resource: collectorTypes.opentelemetryProto.resource.v1.Resource ) { - const libraryInfo = json.node && json.node.libraryInfo; - const serviceInfo = json.node && json.node.serviceInfo; - const identifier = json.node && json.node.identifier; + assert.deepStrictEqual(resource, { + attributes: [ + { + key: 'service.name', + type: 'STRING', + stringValue: 'basic-service', + intValue: '0', + doubleValue: 0, + boolValue: false, + }, + { + key: 'service', + type: 'STRING', + stringValue: 'ui', + intValue: '0', + doubleValue: 0, + boolValue: false, + }, + { + key: 'version', + type: 'DOUBLE', + stringValue: '', + intValue: '0', + doubleValue: 1, + boolValue: false, + }, + { + key: 'cost', + type: 'DOUBLE', + stringValue: '', + intValue: '0', + doubleValue: 112.12, + boolValue: false, + }, + ], + droppedAttributesCount: 0, + }); +} - const language = libraryInfo && libraryInfo.language; - assert.strictEqual(language, languageInfo, 'language is missing'); +export function ensureExportTraceServiceRequestIsSet( + json: collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest +) { + const resourceSpans = json.resourceSpans; + assert.strictEqual( + resourceSpans && resourceSpans.length, + 1, + 'resourceSpans is missing' + ); - const exporterVersion = libraryInfo && libraryInfo.exporterVersion; - assert.strictEqual(exporterVersion, VERSION, 'version is missing'); + const resource = resourceSpans[0].resource; + assert.strictEqual(!!resource, true, 'resource is missing'); - const coreVersion = libraryInfo && libraryInfo.coreLibraryVersion; - assert.strictEqual(coreVersion, core.VERSION, 'core version is missing'); + const instrumentationLibrarySpans = + resourceSpans[0].instrumentationLibrarySpans; + assert.strictEqual( + instrumentationLibrarySpans && instrumentationLibrarySpans.length, + 1, + 'instrumentationLibrarySpans is missing' + ); - const name = serviceInfo && serviceInfo.name; - assert.strictEqual(name, 'bar', 'name is missing'); + const instrumentationLibrary = + instrumentationLibrarySpans[0].instrumentationLibrary; + assert.strictEqual( + !!instrumentationLibrary, + true, + 'instrumentationLibrary is missing' + ); - const hostName = identifier && identifier.hostName; - assert.strictEqual(hostName, 'foo', 'hostName is missing'); + const spans = instrumentationLibrarySpans[0].spans; + assert.strictEqual(spans && spans.length, 1, 'spans are missing'); } diff --git a/packages/opentelemetry-exporter-collector/test/node/CollectorExporter.test.ts b/packages/opentelemetry-exporter-collector/test/node/CollectorExporter.test.ts index f057ce471d..027b3d7af0 100644 --- a/packages/opentelemetry-exporter-collector/test/node/CollectorExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/node/CollectorExporter.test.ts @@ -14,136 +14,117 @@ * limitations under the License. */ -import * as core from '@opentelemetry/core'; -import { ReadableSpan } from '@opentelemetry/tracing'; -import * as http from 'http'; +import * as protoLoader from '@grpc/proto-loader'; +import * as grpc from 'grpc'; +import * as path from 'path'; +import { + BasicTracerProvider, + SimpleSpanProcessor, +} from '@opentelemetry/tracing'; + import * as assert from 'assert'; import * as sinon from 'sinon'; -import { - CollectorExporter, - CollectorExporterConfig, -} from '../../src/CollectorExporter'; +import { CollectorExporter } from '../../src/CollectorExporter'; import * as collectorTypes from '../../src/types'; import { - ensureExportTraceServiceRequestIsSet, - ensureSpanIsCorrect, + ensureResourceIsCorrect, + ensureExportedSpanIsCorrect, mockedReadableSpan, } from '../helper'; -const fakeRequest = { - end: function() {}, - on: function() {}, - write: function() {}, -}; +const traceServiceProtoPath = + 'opentelemetry/proto/collector/trace/v1/trace_service.proto'; +const includeDirs = [path.resolve(__dirname, '../../src/platform/node/protos')]; -const mockRes = { - statusCode: 200, -}; - -const mockResError = { - statusCode: 400, -}; +const address = '127.0.0.1:1501'; describe('CollectorExporter - node', () => { let collectorExporter: CollectorExporter; - let collectorExporterConfig: CollectorExporterConfig; - let spyRequest: any; - let spyWrite: any; - let spans: ReadableSpan[]; - describe('export', () => { - beforeEach(() => { - spyRequest = sinon.stub(http, 'request').returns(fakeRequest as any); - spyWrite = sinon.stub(fakeRequest, 'write'); - collectorExporterConfig = { - hostName: 'foo', - logger: new core.NoopLogger(), - serviceName: 'bar', - attributes: {}, - url: 'http://foo.bar.com', - }; - collectorExporter = new CollectorExporter(collectorExporterConfig); - spans = []; - spans.push(Object.assign({}, mockedReadableSpan)); - }); - afterEach(() => { - spyRequest.restore(); - spyWrite.restore(); - }); - - it('should open the connection', done => { - collectorExporter.export(spans, function() {}); - - setTimeout(() => { - const args = spyRequest.args[0]; - const options = args[0]; - - assert.strictEqual(options.hostname, 'foo.bar.com'); - assert.strictEqual(options.method, 'POST'); - assert.strictEqual(options.path, '/'); + let server: grpc.Server; + let exportedData: + | collectorTypes.opentelemetryProto.trace.v1.ResourceSpans + | undefined; + + before(done => { + server = new grpc.Server(); + protoLoader + .load(traceServiceProtoPath, { + keepCase: false, + longs: String, + enums: String, + defaults: true, + oneofs: true, + includeDirs, + }) + .then((packageDefinition: protoLoader.PackageDefinition) => { + const packageObject: any = grpc.loadPackageDefinition( + packageDefinition + ); + server.addService( + packageObject.opentelemetry.proto.collector.trace.v1.TraceService + .service, + { + Export: (data: { + request: collectorTypes.opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest; + }) => { + try { + exportedData = data.request.resourceSpans[0]; + } catch (e) { + exportedData = undefined; + } + }, + } + ); + server.bind(address, grpc.ServerCredentials.createInsecure()); + server.start(); done(); }); - }); - - it('should successfully send the spans', done => { - collectorExporter.export(spans, function() {}); - - setTimeout(() => { - const writeArgs = spyWrite.args[0]; - const json = JSON.parse( - writeArgs[0] - ) as collectorTypes.ExportTraceServiceRequest; - const span1 = json.spans && json.spans[0]; - assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); - if (span1) { - ensureSpanIsCorrect(span1); - } + }); - ensureExportTraceServiceRequestIsSet(json, 6); + after(() => { + server.forceShutdown(); + }); - done(); - }); + beforeEach(done => { + collectorExporter = new CollectorExporter({ + serviceName: 'basic-service', + url: address, }); - it('should log the successful message', done => { - const spyLoggerDebug = sinon.stub(collectorExporter.logger, 'debug'); - const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); - - const responseSpy = sinon.spy(); - collectorExporter.export(spans, responseSpy); - - setTimeout(() => { - const args = spyRequest.args[0]; - const callback = args[1]; - callback(mockRes); - setTimeout(() => { - const response: any = spyLoggerDebug.args[1][0]; - assert.strictEqual(response, 'statusCode: 200'); - assert.strictEqual(spyLoggerError.args.length, 0); - assert.strictEqual(responseSpy.args[0][0], 0); - done(); - }); - }); - }); + const provider = new BasicTracerProvider(); + provider.addSpanProcessor(new SimpleSpanProcessor(collectorExporter)); + done(); + }); - it('should log the error message', done => { - const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + afterEach(() => { + exportedData = undefined; + }); + describe('export', () => { + it('should export spans', done => { const responseSpy = sinon.spy(); + const spans = [Object.assign({}, mockedReadableSpan)]; collectorExporter.export(spans, responseSpy); - setTimeout(() => { - const args = spyRequest.args[0]; - const callback = args[1]; - callback(mockResError); - setTimeout(() => { - const response: any = spyLoggerError.args[0][0]; - assert.strictEqual(response, 'statusCode: 400'); - - assert.strictEqual(responseSpy.args[0][0], 1); - done(); - }); - }); + assert.ok( + typeof exportedData !== 'undefined', + 'resource' + " doesn't exist" + ); + let spans; + let resource; + if (exportedData) { + spans = exportedData.instrumentationLibrarySpans[0].spans; + resource = exportedData.resource; + ensureExportedSpanIsCorrect(spans[0]); + + assert.ok(typeof resource !== 'undefined', "resource doesn't exist"); + if (resource) { + ensureResourceIsCorrect(resource); + } + } + done(); + }, 200); }); }); });