From f0a3dbabda9a212753e9caf9d8df925be8e9e7e9 Mon Sep 17 00:00:00 2001 From: Bartlomiej Obecny Date: Mon, 6 Jul 2020 21:36:56 +0200 Subject: [PATCH] feat: adding json over http for collector exporter (#1247) * feat: adding json over http for collector exporter * feat: updating readme and adding headers options in config for json over http * chore: reviews and few small cleanups * chore: aligning type for headers * chore: fixing doc * chore: unifying types for headers * chore: reviews * chore: adding validation for headers, and making the types correct this time * chore: linting * chore: linting * chore: fixes after merge * chore: reviews * chore: merge branch 'master' into collector_json --- examples/collector-exporter-node/package.json | 1 + examples/collector-exporter-node/start.js | 8 +- .../README.md | 25 ++- .../src/CollectorTraceExporterBase.ts | 4 +- .../src/enums.ts | 24 +++ .../src/index.ts | 1 + .../browser/CollectorTraceExporter.ts | 12 +- .../platform/node/CollectorTraceExporter.ts | 121 +++++------- .../src/platform/node/types.ts | 2 + .../src/platform/node/utilWithGrpc.ts | 101 ++++++++++ .../src/platform/node/utilWithJson.ts | 84 ++++++++ .../src/transform.ts | 6 +- .../src/types.ts | 1 + .../src/util.ts | 38 ++++ .../browser/CollectorTraceExporter.test.ts | 6 +- .../common/CollectorTraceExporter.test.ts | 4 +- .../test/common/utils.test.ts | 48 +++++ .../node/CollectorExporterWithJson.test.ts | 187 ++++++++++++++++++ .../test/node/CollectorTraceExporter.test.ts | 42 +++- 19 files changed, 622 insertions(+), 93 deletions(-) create mode 100644 packages/opentelemetry-exporter-collector/src/enums.ts create mode 100644 packages/opentelemetry-exporter-collector/src/platform/node/utilWithGrpc.ts create mode 100644 packages/opentelemetry-exporter-collector/src/platform/node/utilWithJson.ts create mode 100644 packages/opentelemetry-exporter-collector/src/util.ts create mode 100644 packages/opentelemetry-exporter-collector/test/common/utils.test.ts create mode 100644 packages/opentelemetry-exporter-collector/test/node/CollectorExporterWithJson.test.ts diff --git a/examples/collector-exporter-node/package.json b/examples/collector-exporter-node/package.json index a30ad9a0b4..5f5d7b39e6 100644 --- a/examples/collector-exporter-node/package.json +++ b/examples/collector-exporter-node/package.json @@ -27,6 +27,7 @@ }, "dependencies": { "@opentelemetry/api": "^0.9.0", + "@opentelemetry/core": "^0.9.0", "@opentelemetry/exporter-collector": "^0.9.0", "@opentelemetry/tracing": "^0.9.0" }, diff --git a/examples/collector-exporter-node/start.js b/examples/collector-exporter-node/start.js index 637a489cf6..3f8f939653 100644 --- a/examples/collector-exporter-node/start.js +++ b/examples/collector-exporter-node/start.js @@ -2,12 +2,14 @@ const opentelemetry = require('@opentelemetry/api'); const { BasicTracerProvider, ConsoleSpanExporter, SimpleSpanProcessor } = require('@opentelemetry/tracing'); -const { CollectorTraceExporter } = require('@opentelemetry/exporter-collector'); +const { CollectorTraceExporter, CollectorProtocolNode } = require('@opentelemetry/exporter-collector'); -const address = '127.0.0.1:55680'; const exporter = new CollectorTraceExporter({ serviceName: 'basic-service', - url: address, + // headers: { + // foo: 'bar' + // }, + protocolNode: CollectorProtocolNode.HTTP_JSON, }); const provider = new BasicTracerProvider(); diff --git a/packages/opentelemetry-exporter-collector/README.md b/packages/opentelemetry-exporter-collector/README.md index 7d198e2392..8cfda2f5a2 100644 --- a/packages/opentelemetry-exporter-collector/README.md +++ b/packages/opentelemetry-exporter-collector/README.md @@ -36,7 +36,7 @@ provider.register(); ``` -## Usage in Node +## Usage in Node - GRPC The CollectorTraceExporter in Node expects the URL to only be the hostname. It will not work with `/v1/trace`. @@ -109,6 +109,29 @@ provider.register(); Note, that this will only work if TLS is also configured on the server. +## Usage in Node - JSON over http + +```js +const { BasicTracerProvider, SimpleSpanProcessor } = require('@opentelemetry/tracing'); +const { CollectorExporter, CollectorTransportNode } = require('@opentelemetry/exporter-collector'); + +const collectorOptions = { + protocolNode: CollectorTransportNode.HTTP_JSON, + serviceName: 'basic-service', + url: '', // url is optional and can be omitted - default is http://localhost:55680/v1/trace + headers: { + foo: 'bar' + }, //an optional object containing custom headers to be sent with each request will only work with json over http +}; + +const provider = new BasicTracerProvider(); +const exporter = new CollectorExporter(collectorOptions); +provider.addSpanProcessor(new SimpleSpanProcessor(exporter)); + +provider.register(); + +``` + ## Running opentelemetry-collector locally to see the traces 1. Go to examples/basic-tracer-node diff --git a/packages/opentelemetry-exporter-collector/src/CollectorTraceExporterBase.ts b/packages/opentelemetry-exporter-collector/src/CollectorTraceExporterBase.ts index 4695c43994..f1508ec0be 100644 --- a/packages/opentelemetry-exporter-collector/src/CollectorTraceExporterBase.ts +++ b/packages/opentelemetry-exporter-collector/src/CollectorTraceExporterBase.ts @@ -43,7 +43,7 @@ export abstract class CollectorTraceExporterBase< */ constructor(config: T = {} as T) { this.serviceName = config.serviceName || DEFAULT_SERVICE_NAME; - this.url = this.getDefaultUrl(config.url); + this.url = this.getDefaultUrl(config); if (typeof config.hostname === 'string') { this.hostname = config.hostname; } @@ -123,5 +123,5 @@ export abstract class CollectorTraceExporterBase< onSuccess: () => void, onError: (error: CollectorExporterError) => void ): void; - abstract getDefaultUrl(url: string | undefined): string; + abstract getDefaultUrl(config: T): string; } diff --git a/packages/opentelemetry-exporter-collector/src/enums.ts b/packages/opentelemetry-exporter-collector/src/enums.ts new file mode 100644 index 0000000000..08c2fd9f2b --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/enums.ts @@ -0,0 +1,24 @@ +/* + * Copyright The 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. + */ + +/** + * Collector transport protocol node options + * Default is GRPC + */ +export enum CollectorProtocolNode { + GRPC, + HTTP_JSON, +} diff --git a/packages/opentelemetry-exporter-collector/src/index.ts b/packages/opentelemetry-exporter-collector/src/index.ts index 52ec5f71f5..79ddd59b38 100644 --- a/packages/opentelemetry-exporter-collector/src/index.ts +++ b/packages/opentelemetry-exporter-collector/src/index.ts @@ -15,3 +15,4 @@ */ export * from './platform'; +export * from './enums'; diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts index 39da94d1d0..661f1e9b87 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/CollectorTraceExporter.ts @@ -19,6 +19,7 @@ import { ReadableSpan } from '@opentelemetry/tracing'; import { toCollectorExportTraceServiceRequest } from '../../transform'; import { CollectorExporterConfigBrowser } from './types'; import * as collectorTypes from '../../types'; +import { parseHeaders } from '../../util'; const DEFAULT_COLLECTOR_URL = 'http://localhost:55680/v1/trace'; @@ -28,10 +29,10 @@ const DEFAULT_COLLECTOR_URL = 'http://localhost:55680/v1/trace'; export class CollectorTraceExporter extends CollectorTraceExporterBase< CollectorExporterConfigBrowser > { - DEFAULT_HEADERS: { [key: string]: string } = { + DEFAULT_HEADERS: Record = { [collectorTypes.OT_REQUEST_HEADER]: '1', }; - private _headers: { [key: string]: string }; + private _headers: Record; private _useXHR: boolean = false; /** @@ -39,7 +40,8 @@ export class CollectorTraceExporter extends CollectorTraceExporterBase< */ constructor(config: CollectorExporterConfigBrowser = {}) { super(config); - this._headers = config.headers || this.DEFAULT_HEADERS; + this._headers = + parseHeaders(config.headers, this.logger) || this.DEFAULT_HEADERS; this._useXHR = !!config.headers || typeof navigator.sendBeacon !== 'function'; } @@ -52,8 +54,8 @@ export class CollectorTraceExporter extends CollectorTraceExporterBase< window.removeEventListener('unload', this.shutdown); } - getDefaultUrl(url: string | undefined) { - return url || DEFAULT_COLLECTOR_URL; + getDefaultUrl(config: CollectorExporterConfigBrowser) { + return config.url || DEFAULT_COLLECTOR_URL; } sendSpans( diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorTraceExporter.ts b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorTraceExporter.ts index 8750315de5..4cdaf9e41d 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/CollectorTraceExporter.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/CollectorTraceExporter.ts @@ -14,23 +14,29 @@ * limitations under the License. */ -import * as protoLoader from '@grpc/proto-loader'; +import { ReadableSpan } from '@opentelemetry/tracing'; import * as grpc from 'grpc'; -import * as path from 'path'; +import { CollectorTraceExporterBase } from '../../CollectorTraceExporterBase'; import * as collectorTypes from '../../types'; -import { ReadableSpan } from '@opentelemetry/tracing'; -import { CollectorTraceExporterBase } from '../../CollectorTraceExporterBase'; -import { CollectorExporterError } from '../../types'; -import { toCollectorExportTraceServiceRequest } from '../../transform'; +import { CollectorProtocolNode } from '../../enums'; +import { parseHeaders } from '../../util'; import { GRPCSpanQueueItem, ServiceClient, CollectorExporterConfigNode, } from './types'; -import { removeProtocol } from './util'; -const DEFAULT_COLLECTOR_URL = 'localhost:55680'; +import { + DEFAULT_COLLECTOR_URL_GRPC, + onInitWithGrpc, + sendSpansUsingGrpc, +} from './utilWithGrpc'; +import { + DEFAULT_COLLECTOR_URL_JSON, + onInitWithJson, + sendSpansUsingJson, +} from './utilWithJson'; /** * Collector Trace Exporter for Node @@ -38,17 +44,39 @@ const DEFAULT_COLLECTOR_URL = 'localhost:55680'; export class CollectorTraceExporter extends CollectorTraceExporterBase< CollectorExporterConfigNode > { + DEFAULT_HEADERS: Record = { + [collectorTypes.OT_REQUEST_HEADER]: '1', + }; isShutDown: boolean = false; traceServiceClient?: ServiceClient = undefined; grpcSpansQueue: GRPCSpanQueueItem[] = []; metadata?: grpc.Metadata; + headers: Record; + private readonly _protocol: CollectorProtocolNode; /** * @param config */ constructor(config: CollectorExporterConfigNode = {}) { super(config); + this._protocol = + typeof config.protocolNode !== 'undefined' + ? config.protocolNode + : CollectorProtocolNode.GRPC; + if (this._protocol === CollectorProtocolNode.HTTP_JSON) { + this.logger.debug('CollectorExporter - using json over http'); + if (config.metadata) { + this.logger.warn('Metadata cannot be set when using json'); + } + } else { + this.logger.debug('CollectorExporter - using grpc'); + if (config.headers) { + this.logger.warn('Headers cannot be set when using grpc'); + } + } this.metadata = config.metadata; + this.headers = + parseHeaders(config.headers, this.logger) || this.DEFAULT_HEADERS; } onShutdown(): void { @@ -60,81 +88,36 @@ export class CollectorTraceExporter extends CollectorTraceExporterBase< onInit(config: CollectorExporterConfigNode): void { this.isShutDown = false; - this.grpcSpansQueue = []; - const serverAddress = removeProtocol(this.url); - const credentials: grpc.ChannelCredentials = - config.credentials || 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 - ); - this.traceServiceClient = new packageObject.opentelemetry.proto.collector.trace.v1.TraceService( - serverAddress, - credentials - ); - if (this.grpcSpansQueue.length > 0) { - const queue = this.grpcSpansQueue.splice(0); - queue.forEach((item: GRPCSpanQueueItem) => { - this.sendSpans(item.spans, item.onSuccess, item.onError); - }); - } - }); + if (config.protocolNode === CollectorProtocolNode.HTTP_JSON) { + onInitWithJson(this, config); + } else { + onInitWithGrpc(this, config); + } } sendSpans( spans: ReadableSpan[], onSuccess: () => void, - onError: (error: CollectorExporterError) => void + onError: (error: collectorTypes.CollectorExporterError) => void ): void { if (this.isShutDown) { this.logger.debug('Shutdown already started. Cannot send spans'); return; } - if (this.traceServiceClient) { - const exportTraceServiceRequest = toCollectorExportTraceServiceRequest( - spans, - this - ); - - this.traceServiceClient.export( - exportTraceServiceRequest, - this.metadata, - (err: collectorTypes.ExportServiceError) => { - if (err) { - this.logger.error( - 'exportTraceServiceRequest', - exportTraceServiceRequest - ); - onError(err); - } else { - onSuccess(); - } - } - ); + if (this._protocol === CollectorProtocolNode.HTTP_JSON) { + sendSpansUsingJson(this, spans, onSuccess, onError); } else { - this.grpcSpansQueue.push({ - spans, - onSuccess, - onError, - }); + sendSpansUsingGrpc(this, spans, onSuccess, onError); } } - getDefaultUrl(url: string | undefined): string { - return url || DEFAULT_COLLECTOR_URL; + getDefaultUrl(config: CollectorExporterConfigNode): string { + if (!config.url) { + return config.protocolNode === CollectorProtocolNode.HTTP_JSON + ? DEFAULT_COLLECTOR_URL_JSON + : DEFAULT_COLLECTOR_URL_GRPC; + } + return config.url; } } diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/types.ts b/packages/opentelemetry-exporter-collector/src/platform/node/types.ts index 507f478f44..a7eb8c74b0 100644 --- a/packages/opentelemetry-exporter-collector/src/platform/node/types.ts +++ b/packages/opentelemetry-exporter-collector/src/platform/node/types.ts @@ -16,6 +16,7 @@ import * as grpc from 'grpc'; import { ReadableSpan } from '@opentelemetry/tracing'; +import { CollectorProtocolNode } from '../../enums'; import { CollectorExporterError, CollectorExporterConfigBase, @@ -49,4 +50,5 @@ export interface CollectorExporterConfigNode extends CollectorExporterConfigBase { credentials?: grpc.ChannelCredentials; metadata?: grpc.Metadata; + protocolNode?: CollectorProtocolNode; } diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/utilWithGrpc.ts b/packages/opentelemetry-exporter-collector/src/platform/node/utilWithGrpc.ts new file mode 100644 index 0000000000..49ca20fc87 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/node/utilWithGrpc.ts @@ -0,0 +1,101 @@ +/* + * Copyright The 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 protoLoader from '@grpc/proto-loader'; +import * as grpc from 'grpc'; +import * as path from 'path'; +import * as collectorTypes from '../../types'; + +import { ReadableSpan } from '@opentelemetry/tracing'; +import { toCollectorExportTraceServiceRequest } from '../../transform'; +import { CollectorTraceExporter } from './CollectorTraceExporter'; +import { CollectorExporterConfigNode, GRPCSpanQueueItem } from './types'; +import { removeProtocol } from './util'; + +export const DEFAULT_COLLECTOR_URL_GRPC = 'localhost:55680'; + +export function onInitWithGrpc( + collector: CollectorTraceExporter, + config: CollectorExporterConfigNode +): void { + collector.grpcSpansQueue = []; + const serverAddress = removeProtocol(collector.url); + const credentials: grpc.ChannelCredentials = + config.credentials || 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); + collector.traceServiceClient = new packageObject.opentelemetry.proto.collector.trace.v1.TraceService( + serverAddress, + credentials + ); + if (collector.grpcSpansQueue.length > 0) { + const queue = collector.grpcSpansQueue.splice(0); + queue.forEach((item: GRPCSpanQueueItem) => { + collector.sendSpans(item.spans, item.onSuccess, item.onError); + }); + } + }); +} + +export function sendSpansUsingGrpc( + collector: CollectorTraceExporter, + spans: ReadableSpan[], + onSuccess: () => void, + onError: (error: collectorTypes.CollectorExporterError) => void +): void { + if (collector.traceServiceClient) { + const exportTraceServiceRequest = toCollectorExportTraceServiceRequest( + spans, + collector + ); + collector.traceServiceClient.export( + exportTraceServiceRequest, + collector.metadata, + (err: collectorTypes.ExportServiceError) => { + if (err) { + collector.logger.error( + 'exportTraceServiceRequest', + exportTraceServiceRequest + ); + onError(err); + } else { + collector.logger.debug('spans sent'); + onSuccess(); + } + } + ); + } else { + collector.grpcSpansQueue.push({ + spans, + onSuccess, + onError, + }); + } +} diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/utilWithJson.ts b/packages/opentelemetry-exporter-collector/src/platform/node/utilWithJson.ts new file mode 100644 index 0000000000..a37638ac12 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/node/utilWithJson.ts @@ -0,0 +1,84 @@ +/* + * Copyright The 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 url from 'url'; +import * as http from 'http'; +import * as https from 'https'; + +import { ReadableSpan } from '@opentelemetry/tracing'; +import * as collectorTypes from '../../types'; +import { toCollectorExportTraceServiceRequest } from '../../transform'; +import { CollectorTraceExporter } from './CollectorTraceExporter'; +import { CollectorExporterConfigNode } from './types'; + +export const DEFAULT_COLLECTOR_URL_JSON = 'http://localhost:55680/v1/trace'; + +export function onInitWithJson( + _collector: CollectorTraceExporter, + _config: CollectorExporterConfigNode +): void { + // nothing to be done for json yet +} + +export function sendSpansUsingJson( + collector: CollectorTraceExporter, + spans: ReadableSpan[], + onSuccess: () => void, + onError: (error: collectorTypes.CollectorExporterError) => void +): void { + const exportTraceServiceRequest = toCollectorExportTraceServiceRequest( + spans, + collector + ); + + const body = JSON.stringify(exportTraceServiceRequest); + const parsedUrl = new url.URL(collector.url); + + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port, + path: parsedUrl.pathname, + method: 'POST', + headers: { + 'Content-Length': Buffer.byteLength(body), + 'Content-Type': 'application/json', + ...collector.headers, + }, + }; + + const request = parsedUrl.protocol === 'http:' ? http.request : https.request; + const req = request(options, (res: http.IncomingMessage) => { + if (res.statusCode && res.statusCode < 299) { + collector.logger.debug(`statusCode: ${res.statusCode}`); + onSuccess(); + } else { + collector.logger.error(`statusCode: ${res.statusCode}`); + onError({ + code: res.statusCode, + message: res.statusMessage, + }); + } + }); + + req.on('error', (error: Error) => { + collector.logger.error('error', error.message); + onError({ + message: error.message, + }); + }); + req.write(body); + req.end(); +} diff --git a/packages/opentelemetry-exporter-collector/src/transform.ts b/packages/opentelemetry-exporter-collector/src/transform.ts index 626169d077..0e5d0c1f96 100644 --- a/packages/opentelemetry-exporter-collector/src/transform.ts +++ b/packages/opentelemetry-exporter-collector/src/transform.ts @@ -152,7 +152,7 @@ export function toCollectorSpan( */ export function toCollectorResource( resource?: Resource, - additionalAttributes: { [key: string]: any } = {} + additionalAttributes: { [key: string]: unknown } = {} ): opentelemetryProto.resource.v1.Resource { const attr = Object.assign( {}, @@ -195,14 +195,12 @@ export function toCollectorTraceState( * Prepares trace service request to be sent to collector * @param spans spans * @param collectorExporterBase - * @param [name] Instrumentation Library Name */ export function toCollectorExportTraceServiceRequest< T extends CollectorExporterConfigBase >( spans: ReadableSpan[], - collectorExporterBase: CollectorTraceExporterBase, - name = '' + collectorExporterBase: CollectorTraceExporterBase ): opentelemetryProto.collector.trace.v1.ExportTraceServiceRequest { const groupedSpans: Map< Resource, diff --git a/packages/opentelemetry-exporter-collector/src/types.ts b/packages/opentelemetry-exporter-collector/src/types.ts index 68aa4fbf57..907ba60276 100644 --- a/packages/opentelemetry-exporter-collector/src/types.ts +++ b/packages/opentelemetry-exporter-collector/src/types.ts @@ -271,6 +271,7 @@ export interface ExportServiceError { * Collector Exporter base config */ export interface CollectorExporterConfigBase { + headers?: Partial>; hostname?: string; logger?: Logger; serviceName?: string; diff --git a/packages/opentelemetry-exporter-collector/src/util.ts b/packages/opentelemetry-exporter-collector/src/util.ts new file mode 100644 index 0000000000..1cb1b18aae --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/util.ts @@ -0,0 +1,38 @@ +/* + * Copyright The 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 { Logger } from '@opentelemetry/api'; +import { NoopLogger } from '@opentelemetry/core'; + +/** + * Parses headers from config leaving only those that have defined values + * @param partialHeaders + * @param logger + */ +export function parseHeaders( + partialHeaders: Partial> = {}, + logger: Logger = new NoopLogger() +): Record { + const headers: Record = {}; + Object.entries(partialHeaders).forEach(([key, value]) => { + if (typeof value !== 'undefined') { + headers[key] = String(value); + } else { + logger.warn(`Header "${key}" has wrong value and will be ignored`); + } + }); + return headers; +} diff --git a/packages/opentelemetry-exporter-collector/test/browser/CollectorTraceExporter.test.ts b/packages/opentelemetry-exporter-collector/test/browser/CollectorTraceExporter.test.ts index 2e11e341df..4893ddcb98 100644 --- a/packages/opentelemetry-exporter-collector/test/browser/CollectorTraceExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/browser/CollectorTraceExporter.test.ts @@ -34,9 +34,9 @@ const sendBeacon = navigator.sendBeacon; describe('CollectorTraceExporter - web', () => { let collectorTraceExporter: CollectorTraceExporter; let collectorExporterConfig: CollectorExporterConfigBrowser; - let spyOpen: any; - let spySend: any; - let spyBeacon: any; + let spyOpen: sinon.SinonSpy; + let spySend: sinon.SinonSpy; + let spyBeacon: sinon.SinonSpy; let spans: ReadableSpan[]; beforeEach(() => { diff --git a/packages/opentelemetry-exporter-collector/test/common/CollectorTraceExporter.test.ts b/packages/opentelemetry-exporter-collector/test/common/CollectorTraceExporter.test.ts index 504aff4338..69c4a15de3 100644 --- a/packages/opentelemetry-exporter-collector/test/common/CollectorTraceExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/common/CollectorTraceExporter.test.ts @@ -29,8 +29,8 @@ class CollectorTraceExporter extends CollectorTraceExporterBase< onInit() {} onShutdown() {} sendSpans() {} - getDefaultUrl(url: string | undefined) { - return url || ''; + getDefaultUrl(config: CollectorExporterConfig) { + return config.url || ''; } } diff --git a/packages/opentelemetry-exporter-collector/test/common/utils.test.ts b/packages/opentelemetry-exporter-collector/test/common/utils.test.ts new file mode 100644 index 0000000000..b5fb8d3507 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/test/common/utils.test.ts @@ -0,0 +1,48 @@ +/* + * Copyright The 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 assert from 'assert'; +import * as sinon from 'sinon'; +import { NoopLogger } from '@opentelemetry/core'; +import { parseHeaders } from '../../src/util'; + +describe('utils', () => { + describe('parseHeaders', () => { + it('should ignore undefined headers', () => { + const logger = new NoopLogger(); + const spyWarn = sinon.stub(logger, 'warn'); + const headers: Partial> = { + foo1: undefined, + foo2: 'bar', + foo3: 1, + }; + const result = parseHeaders(headers, logger); + assert.deepStrictEqual(result, { + foo2: 'bar', + foo3: '1', + }); + const args = spyWarn.args[0]; + assert.strictEqual( + args[0], + 'Header "foo1" has wrong value and will be ignored' + ); + }); + it('should parse undefined', () => { + const result = parseHeaders(undefined); + assert.deepStrictEqual(result, {}); + }); + }); +}); diff --git a/packages/opentelemetry-exporter-collector/test/node/CollectorExporterWithJson.test.ts b/packages/opentelemetry-exporter-collector/test/node/CollectorExporterWithJson.test.ts new file mode 100644 index 0000000000..3b21ea2c63 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/test/node/CollectorExporterWithJson.test.ts @@ -0,0 +1,187 @@ +/* + * Copyright The 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 core from '@opentelemetry/core'; +import { ReadableSpan } from '@opentelemetry/tracing'; +import * as http from 'http'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { CollectorProtocolNode } from '../../src/enums'; +import { CollectorTraceExporter } from '../../src/platform/node'; +import { CollectorExporterConfigNode } from '../../src/platform/node/types'; +import * as collectorTypes from '../../src/types'; + +import { + ensureExportTraceServiceRequestIsSet, + ensureSpanIsCorrect, + mockedReadableSpan, +} from '../helper'; + +const fakeRequest = { + end: function () {}, + on: function () {}, + write: function () {}, +}; + +const mockRes = { + statusCode: 200, +}; + +const mockResError = { + statusCode: 400, +}; + +describe('CollectorExporter - node with json over http', () => { + let collectorExporter: CollectorTraceExporter; + let collectorExporterConfig: CollectorExporterConfigNode; + let spyRequest: sinon.SinonSpy; + let spyWrite: sinon.SinonSpy; + let spans: ReadableSpan[]; + describe('export', () => { + beforeEach(() => { + spyRequest = sinon.stub(http, 'request').returns(fakeRequest as any); + spyWrite = sinon.stub(fakeRequest, 'write'); + collectorExporterConfig = { + headers: { + foo: 'bar', + }, + protocolNode: CollectorProtocolNode.HTTP_JSON, + hostname: 'foo', + logger: new core.NoopLogger(), + serviceName: 'bar', + attributes: {}, + url: 'http://foo.bar.com', + }; + collectorExporter = new CollectorTraceExporter(collectorExporterConfig); + spans = []; + spans.push(Object.assign({}, mockedReadableSpan)); + }); + afterEach(() => { + spyRequest.restore(); + spyWrite.restore(); + }); + + it('should open the connection', done => { + collectorExporter.export(spans, () => {}); + + 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, '/'); + done(); + }); + }); + + it('should set custom headers', done => { + collectorExporter.export(spans, () => {}); + + setTimeout(() => { + const args = spyRequest.args[0]; + const options = args[0]; + assert.strictEqual(options.headers['foo'], 'bar'); + done(); + }); + }); + + it('should successfully send the spans', done => { + collectorExporter.export(spans, () => {}); + + setTimeout(() => { + const writeArgs = spyWrite.args[0]; + const json = JSON.parse( + writeArgs[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); + } + + ensureExportTraceServiceRequestIsSet(json); + + done(); + }); + }); + + 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(); + }); + }); + }); + + it('should log the error message', done => { + 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(mockResError); + setTimeout(() => { + const response: any = spyLoggerError.args[0][0]; + assert.strictEqual(response, 'statusCode: 400'); + + assert.strictEqual(responseSpy.args[0][0], 1); + done(); + }); + }); + }); + }); + describe('CollectorTraceExporter - node (getDefaultUrl)', () => { + it('should default to localhost', done => { + const collectorExporter = new CollectorTraceExporter({ + protocolNode: CollectorProtocolNode.HTTP_JSON, + }); + setTimeout(() => { + assert.strictEqual( + collectorExporter['url'], + 'http://localhost:55680/v1/trace' + ); + done(); + }); + }); + + it('should keep the URL if included', done => { + const url = 'http://foo.bar.com'; + const collectorExporter = new CollectorTraceExporter({ url }); + setTimeout(() => { + assert.strictEqual(collectorExporter['url'], url); + done(); + }); + }); + }); +}); diff --git a/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporter.test.ts b/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporter.test.ts index 514125e882..91e89fc02f 100644 --- a/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporter.test.ts +++ b/packages/opentelemetry-exporter-collector/test/node/CollectorTraceExporter.test.ts @@ -15,23 +15,25 @@ */ import * as protoLoader from '@grpc/proto-loader'; -import * as grpc from 'grpc'; -import * as path from 'path'; -import * as fs from 'fs'; +import { ConsoleLogger, LogLevel } from '@opentelemetry/core'; import { BasicTracerProvider, SimpleSpanProcessor, } from '@opentelemetry/tracing'; import * as assert from 'assert'; +import * as fs from 'fs'; +import * as grpc from 'grpc'; +import * as path from 'path'; import * as sinon from 'sinon'; +import { CollectorProtocolNode } from '../../src'; import { CollectorTraceExporter } from '../../src/platform/node'; import * as collectorTypes from '../../src/types'; import { - ensureResourceIsCorrect, ensureExportedSpanIsCorrect, ensureMetadataIsCorrect, + ensureResourceIsCorrect, mockedReadableSpan, } from '../helper'; @@ -138,6 +140,38 @@ const testCollectorExporter = (params: TestParams) => reqMetadata = undefined; }); + describe('instance', () => { + it('should warn about headers when using grpc', () => { + const logger = new ConsoleLogger(LogLevel.DEBUG); + const spyLoggerWarn = sinon.stub(logger, 'warn'); + collectorExporter = new CollectorTraceExporter({ + logger, + serviceName: 'basic-service', + url: address, + headers: { + foo: 'bar', + }, + }); + const args = spyLoggerWarn.args[0]; + assert.strictEqual(args[0], 'Headers cannot be set when using grpc'); + }); + it('should warn about metadata when using json', () => { + const metadata = new grpc.Metadata(); + metadata.set('k', 'v'); + const logger = new ConsoleLogger(LogLevel.DEBUG); + const spyLoggerWarn = sinon.stub(logger, 'warn'); + collectorExporter = new CollectorTraceExporter({ + logger, + serviceName: 'basic-service', + url: address, + metadata, + protocolNode: CollectorProtocolNode.HTTP_JSON, + }); + const args = spyLoggerWarn.args[0]; + assert.strictEqual(args[0], 'Metadata cannot be set when using json'); + }); + }); + describe('export', () => { it('should export spans', done => { const responseSpy = sinon.spy();