diff --git a/examples/tracer-web/examples/zipkin/index.html b/examples/tracer-web/examples/zipkin/index.html new file mode 100644 index 0000000000..bf98b07433 --- /dev/null +++ b/examples/tracer-web/examples/zipkin/index.html @@ -0,0 +1,19 @@ + + + + + + Zipkin Exporter Example + + + + + + Example of using Web Tracer with Zipkin Exporter + +
+ + + + + diff --git a/examples/tracer-web/examples/zipkin/index.js b/examples/tracer-web/examples/zipkin/index.js new file mode 100644 index 0000000000..410749b00a --- /dev/null +++ b/examples/tracer-web/examples/zipkin/index.js @@ -0,0 +1,23 @@ +import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; +import { WebTracerProvider } from '@opentelemetry/web'; +import { ZipkinExporter } from '@opentelemetry/exporter-zipkin'; + +const provider = new WebTracerProvider(); +provider.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); +provider.addSpanProcessor(new SimpleSpanProcessor(new ZipkinExporter())); + +provider.register(); + +const tracer = provider.getTracer('example-tracer-web'); + +const prepareClickEvent = () => { + const element = document.getElementById('button1'); + + const onClick = () => { + const span = tracer.startSpan('foo'); + span.end(); + }; + element.addEventListener('click', onClick); +}; + +window.addEventListener('load', prepareClickEvent); diff --git a/examples/tracer-web/package.json b/examples/tracer-web/package.json index 005ca3216a..711d2e5292 100644 --- a/examples/tracer-web/package.json +++ b/examples/tracer-web/package.json @@ -37,6 +37,7 @@ "@opentelemetry/context-zone": "^0.11.0", "@opentelemetry/core": "^0.11.0", "@opentelemetry/exporter-collector": "^0.11.0", + "@opentelemetry/exporter-zipkin": "^0.11.0", "@opentelemetry/metrics": "^0.11.0", "@opentelemetry/plugin-document-load": "^0.9.0", "@opentelemetry/plugin-fetch": "^0.11.0", diff --git a/examples/tracer-web/webpack.config.js b/examples/tracer-web/webpack.config.js index 3859f2f8fd..00773db28b 100644 --- a/examples/tracer-web/webpack.config.js +++ b/examples/tracer-web/webpack.config.js @@ -12,6 +12,7 @@ const common = { fetch: 'examples/fetch/index.js', 'xml-http-request': 'examples/xml-http-request/index.js', 'user-interaction': 'examples/user-interaction/index.js', + zipkin: 'examples/zipkin/index.js', }, output: { path: path.resolve(__dirname, 'dist'), diff --git a/packages/opentelemetry-exporter-zipkin/.eslintrc.js b/packages/opentelemetry-exporter-zipkin/.eslintrc.js index f726f3becb..9dfe62f9b8 100644 --- a/packages/opentelemetry-exporter-zipkin/.eslintrc.js +++ b/packages/opentelemetry-exporter-zipkin/.eslintrc.js @@ -1,7 +1,9 @@ module.exports = { "env": { "mocha": true, - "node": true + "commonjs": true, + "node": true, + "browser": true }, ...require('../../eslint.config.js') } diff --git a/packages/opentelemetry-exporter-zipkin/README.md b/packages/opentelemetry-exporter-zipkin/README.md index ccd4404c67..1c1466ae5e 100644 --- a/packages/opentelemetry-exporter-zipkin/README.md +++ b/packages/opentelemetry-exporter-zipkin/README.md @@ -16,7 +16,7 @@ OpenTelemetry Zipkin Trace Exporter allows the user to send collected traces to npm install --save @opentelemetry/exporter-zipkin ``` -## Usage +## Usage in Node and Browser Install the exporter on your application and pass the options. `serviceName` is an optional string. If omitted, the exporter will first try to get the service name from the Resource. If no service name can be detected on the Resource, a fallback name of "OpenTelemetry Service" will be used. diff --git a/packages/opentelemetry-exporter-zipkin/karma.conf.js b/packages/opentelemetry-exporter-zipkin/karma.conf.js new file mode 100644 index 0000000000..455b1437c8 --- /dev/null +++ b/packages/opentelemetry-exporter-zipkin/karma.conf.js @@ -0,0 +1,26 @@ +/*! + * 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 + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const karmaWebpackConfig = require('../../karma.webpack'); +const karmaBaseConfig = require('../../karma.base'); + +module.exports = (config) => { + config.set(Object.assign({}, karmaBaseConfig, { + webpack: karmaWebpackConfig, + files: ['test/browser/index-webpack.ts'], + preprocessors: { 'test/browser/index-webpack.ts': ['webpack'] } + })) +}; diff --git a/packages/opentelemetry-exporter-zipkin/package.json b/packages/opentelemetry-exporter-zipkin/package.json index a662012391..57d002ea48 100644 --- a/packages/opentelemetry-exporter-zipkin/package.json +++ b/packages/opentelemetry-exporter-zipkin/package.json @@ -5,21 +5,29 @@ "main": "build/src/index.js", "types": "build/src/index.d.ts", "repository": "open-telemetry/opentelemetry-js", + "browser": { + "./src/platform/index.ts": "./src/platform/browser/index.ts", + "./build/src/platform/index.js": "./build/src/platform/browser/index.js" + }, "scripts": { - "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.test.ts'", - "tdd": "npm run test -- --watch-extensions ts --watch", "clean": "rimraf build/*", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "codecov:browser": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "compile": "npm run version:update && tsc -p .", "lint": "eslint . --ext .ts", "lint:fix": "eslint . --ext .ts --fix", - "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", "precompile": "tsc --version", + "prepare": "npm run compile", + "tdd": "npm run test -- --watch-extensions ts --watch", + "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", - "compile": "npm run version:update && tsc -p .", - "prepare": "npm run compile" + "watch": "tsc -w" }, "keywords": [ "opentelemetry", "nodejs", + "browser", "tracing", "profiling" ], @@ -40,17 +48,33 @@ "access": "public" }, "devDependencies": { + "@babel/core": "7.11.0", "@types/mocha": "8.0.2", "@types/node": "14.0.27", + "@types/sinon": "9.0.4", + "@types/webpack-env": "1.15.2", + "babel-loader": "8.1.0", "codecov": "3.7.2", "gts": "2.0.2", + "istanbul-instrumenter-loader": "3.0.1", + "karma": "5.1.1", + "karma-chrome-launcher": "3.1.0", + "karma-coverage-istanbul-reporter": "3.0.3", + "karma-mocha": "2.0.1", + "karma-spec-reporter": "0.0.32", + "karma-webpack": "4.0.2", "mocha": "7.2.0", "nock": "12.0.3", "nyc": "15.1.0", "rimraf": "3.0.2", + "sinon": "9.0.2", + "ts-loader": "8.0.1", "ts-mocha": "7.0.0", "ts-node": "8.10.2", - "typescript": "3.9.7" + "typescript": "3.9.7", + "webpack": "4.44.1", + "webpack-cli": "3.3.12", + "webpack-merge": "5.0.9" }, "dependencies": { "@opentelemetry/api": "^0.11.0", diff --git a/packages/opentelemetry-exporter-zipkin/src/index.ts b/packages/opentelemetry-exporter-zipkin/src/index.ts index 9a35ec9d07..4879fea44f 100644 --- a/packages/opentelemetry-exporter-zipkin/src/index.ts +++ b/packages/opentelemetry-exporter-zipkin/src/index.ts @@ -15,3 +15,4 @@ */ export * from './zipkin'; +export * from './platform'; diff --git a/packages/opentelemetry-exporter-zipkin/test/e2e.test.ts b/packages/opentelemetry-exporter-zipkin/src/platform/browser/index.ts similarity index 87% rename from packages/opentelemetry-exporter-zipkin/test/e2e.test.ts rename to packages/opentelemetry-exporter-zipkin/src/platform/browser/index.ts index 96181daa51..3786b94827 100644 --- a/packages/opentelemetry-exporter-zipkin/test/e2e.test.ts +++ b/packages/opentelemetry-exporter-zipkin/src/platform/browser/index.ts @@ -14,6 +14,4 @@ * limitations under the License. */ -describe('Zipkin Exporter E2E', () => { - it('should report spans to Zipkin server'); -}); +export * from './util'; diff --git a/packages/opentelemetry-exporter-zipkin/src/platform/browser/util.ts b/packages/opentelemetry-exporter-zipkin/src/platform/browser/util.ts new file mode 100644 index 0000000000..a954fb2d42 --- /dev/null +++ b/packages/opentelemetry-exporter-zipkin/src/platform/browser/util.ts @@ -0,0 +1,131 @@ +/* + * 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 api from '@opentelemetry/api'; +import { ExportResult } from '@opentelemetry/core'; +import * as zipkinTypes from '../../types'; +import { OT_REQUEST_HEADER } from '../../utils'; + +/** + * Prepares send function that will send spans to the remote Zipkin service. + */ +export function prepareSend( + logger: api.Logger, + urlStr: string, + headers?: Record +) { + let xhrHeaders: Record; + const useBeacon = navigator.sendBeacon && !headers; + if (headers) { + xhrHeaders = { + Accept: 'application/json', + 'Content-Type': 'application/json', + [OT_REQUEST_HEADER]: '1', + ...headers, + }; + } + + /** + * Send spans to the remote Zipkin service. + */ + return function send( + zipkinSpans: zipkinTypes.Span[], + done: (result: ExportResult) => void + ) { + if (zipkinSpans.length === 0) { + logger.debug('Zipkin send with empty spans'); + return done(ExportResult.SUCCESS); + } + const payload = JSON.stringify(zipkinSpans); + if (useBeacon) { + sendWithBeacon(payload, done, urlStr, logger); + } else { + sendWithXhr(payload, done, urlStr, logger, xhrHeaders); + } + }; +} + +/** + * Sends data using beacon + * @param data + * @param done + * @param urlStr + * @param logger + */ +function sendWithBeacon( + data: string, + done: (result: ExportResult) => void, + urlStr: string, + logger: api.Logger +) { + if (navigator.sendBeacon(urlStr, data)) { + logger.debug('sendBeacon - can send', data); + done(ExportResult.SUCCESS); + } else { + logger.error('sendBeacon - cannot send', data); + done(ExportResult.FAILED_NOT_RETRYABLE); + } +} + +/** + * Sends data using XMLHttpRequest + * @param data + * @param done + * @param urlStr + * @param logger + * @param xhrHeaders + */ +function sendWithXhr( + data: string, + done: (result: ExportResult) => void, + urlStr: string, + logger: api.Logger, + xhrHeaders: Record = {} +) { + const xhr = new window.XMLHttpRequest(); + xhr.open('POST', urlStr); + Object.entries(xhrHeaders).forEach(([k, v]) => { + xhr.setRequestHeader(k, v); + }); + + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + const statusCode = xhr.status || 0; + logger.debug( + 'Zipkin response status code: %d, body: %s', + statusCode, + data + ); + + if (xhr.status >= 200 && xhr.status < 400) { + return done(ExportResult.SUCCESS); + } else if (statusCode < 500) { + return done(ExportResult.FAILED_NOT_RETRYABLE); + } else { + return done(ExportResult.FAILED_RETRYABLE); + } + } + }; + + xhr.onerror = err => { + logger.error('Zipkin request error', err); + return done(ExportResult.FAILED_RETRYABLE); + }; + + // Issue request to remote service + logger.debug('Zipkin request payload: %s', data); + xhr.send(data); +} diff --git a/packages/opentelemetry-exporter-zipkin/src/platform/index.ts b/packages/opentelemetry-exporter-zipkin/src/platform/index.ts new file mode 100644 index 0000000000..cdaf8858ce --- /dev/null +++ b/packages/opentelemetry-exporter-zipkin/src/platform/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export * from './node'; diff --git a/packages/opentelemetry-exporter-zipkin/src/platform/node/index.ts b/packages/opentelemetry-exporter-zipkin/src/platform/node/index.ts new file mode 100644 index 0000000000..3786b94827 --- /dev/null +++ b/packages/opentelemetry-exporter-zipkin/src/platform/node/index.ts @@ -0,0 +1,17 @@ +/* + * 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. + */ + +export * from './util'; diff --git a/packages/opentelemetry-exporter-zipkin/src/platform/node/util.ts b/packages/opentelemetry-exporter-zipkin/src/platform/node/util.ts new file mode 100644 index 0000000000..89f740b8e2 --- /dev/null +++ b/packages/opentelemetry-exporter-zipkin/src/platform/node/util.ts @@ -0,0 +1,97 @@ +/* + * 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 api from '@opentelemetry/api'; +import { ExportResult } from '@opentelemetry/core'; +import * as http from 'http'; +import * as https from 'https'; +import * as url from 'url'; +import * as zipkinTypes from '../../types'; +import { OT_REQUEST_HEADER } from '../../utils'; + +/** + * Prepares send function that will send spans to the remote Zipkin service. + */ +export function prepareSend( + logger: api.Logger, + urlStr: string, + headers?: Record +) { + const urlOpts = url.parse(urlStr); + + const reqOpts: http.RequestOptions = Object.assign( + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + [OT_REQUEST_HEADER]: 1, + ...headers, + }, + }, + urlOpts + ); + + /** + * Send spans to the remote Zipkin service. + */ + return function send( + zipkinSpans: zipkinTypes.Span[], + done: (result: ExportResult) => void + ) { + if (zipkinSpans.length === 0) { + logger.debug('Zipkin send with empty spans'); + return done(ExportResult.SUCCESS); + } + + const { request } = reqOpts.protocol === 'http:' ? http : https; + const req = request(reqOpts, (res: http.IncomingMessage) => { + let rawData = ''; + res.on('data', chunk => { + rawData += chunk; + }); + res.on('end', () => { + const statusCode = res.statusCode || 0; + logger.debug( + 'Zipkin response status code: %d, body: %s', + statusCode, + rawData + ); + + // Consider 2xx and 3xx as success. + if (statusCode < 400) { + return done(ExportResult.SUCCESS); + // Consider 4xx as failed non-retriable. + } else if (statusCode < 500) { + return done(ExportResult.FAILED_NOT_RETRYABLE); + // Consider 5xx as failed retriable. + } else { + return done(ExportResult.FAILED_RETRYABLE); + } + }); + }); + + req.on('error', (err: Error) => { + logger.error('Zipkin request error', err); + return done(ExportResult.FAILED_RETRYABLE); + }); + + // Issue request to remote service + const payload = JSON.stringify(zipkinSpans); + logger.debug('Zipkin request payload: %s', payload); + req.write(payload, 'utf8'); + req.end(); + }; +} diff --git a/packages/opentelemetry-exporter-zipkin/src/types.ts b/packages/opentelemetry-exporter-zipkin/src/types.ts index 0433351491..7eb7eaeaf2 100644 --- a/packages/opentelemetry-exporter-zipkin/src/types.ts +++ b/packages/opentelemetry-exporter-zipkin/src/types.ts @@ -15,6 +15,7 @@ */ import * as api from '@opentelemetry/api'; +import { ExportResult } from '@opentelemetry/core'; /** * Exporter config @@ -177,3 +178,11 @@ export enum SpanKind { CONSUMER = 'CONSUMER', PRODUCER = 'PRODUCER', } + +/** + * interface for function that will send zipkin spans + */ +export type SendFunction = ( + zipkinSpans: Span[], + done: (result: ExportResult) => void +) => void; diff --git a/packages/opentelemetry-exporter-zipkin/src/zipkin.ts b/packages/opentelemetry-exporter-zipkin/src/zipkin.ts index 54010b3af3..18188565c4 100644 --- a/packages/opentelemetry-exporter-zipkin/src/zipkin.ts +++ b/packages/opentelemetry-exporter-zipkin/src/zipkin.ts @@ -15,19 +15,17 @@ */ import * as api from '@opentelemetry/api'; -import * as http from 'http'; -import * as https from 'https'; -import * as url from 'url'; import { ExportResult, NoopLogger } from '@opentelemetry/core'; import { SpanExporter, ReadableSpan } from '@opentelemetry/tracing'; +import { prepareSend } from './platform/index'; import * as zipkinTypes from './types'; import { toZipkinSpan, statusCodeTagName, statusDescriptionTagName, } from './transform'; -import { OT_REQUEST_HEADER } from './utils'; import { SERVICE_RESOURCE } from '@opentelemetry/resources'; + /** * Zipkin Exporter */ @@ -37,26 +35,14 @@ export class ZipkinExporter implements SpanExporter { private readonly _logger: api.Logger; private readonly _statusCodeTagName: string; private readonly _statusDescriptionTagName: string; - private readonly _reqOpts: http.RequestOptions; + private _send: zipkinTypes.SendFunction; private _serviceName?: string; private _isShutdown: boolean; constructor(config: zipkinTypes.ExporterConfig = {}) { const urlStr = config.url || ZipkinExporter.DEFAULT_URL; - const urlOpts = url.parse(urlStr); - this._logger = config.logger || new NoopLogger(); - this._reqOpts = Object.assign( - { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - [OT_REQUEST_HEADER]: 1, - ...config.headers, - }, - }, - urlOpts - ); + this._send = prepareSend(this._logger, urlStr, config.headers); this._serviceName = config.serviceName; this._statusCodeTagName = config.statusCodeTagName || statusCodeTagName; this._statusDescriptionTagName = @@ -118,55 +104,4 @@ export class ZipkinExporter implements SpanExporter { } }); } - - /** - * Send spans to the remote Zipkin service. - */ - private _send( - zipkinSpans: zipkinTypes.Span[], - done: (result: ExportResult) => void - ) { - if (zipkinSpans.length === 0) { - this._logger.debug('Zipkin send with empty spans'); - return done(ExportResult.SUCCESS); - } - - const { request } = this._reqOpts.protocol === 'http:' ? http : https; - const req = request(this._reqOpts, (res: http.IncomingMessage) => { - let rawData = ''; - res.on('data', chunk => { - rawData += chunk; - }); - res.on('end', () => { - const statusCode = res.statusCode || 0; - this._logger.debug( - 'Zipkin response status code: %d, body: %s', - statusCode, - rawData - ); - - // Consider 2xx and 3xx as success. - if (statusCode < 400) { - return done(ExportResult.SUCCESS); - // Consider 4xx as failed non-retriable. - } else if (statusCode < 500) { - return done(ExportResult.FAILED_NOT_RETRYABLE); - // Consider 5xx as failed retriable. - } else { - return done(ExportResult.FAILED_RETRYABLE); - } - }); - }); - - req.on('error', (err: Error) => { - this._logger.error('Zipkin request error', err); - return done(ExportResult.FAILED_RETRYABLE); - }); - - // Issue request to remote service - const payload = JSON.stringify(zipkinSpans); - this._logger.debug('Zipkin request payload: %s', payload); - req.write(payload, 'utf8'); - req.end(); - } } diff --git a/packages/opentelemetry-exporter-zipkin/test/browser/index-webpack.ts b/packages/opentelemetry-exporter-zipkin/test/browser/index-webpack.ts new file mode 100644 index 0000000000..99100a0f6e --- /dev/null +++ b/packages/opentelemetry-exporter-zipkin/test/browser/index-webpack.ts @@ -0,0 +1,23 @@ +/* + * 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. + */ +const testsContext = require.context('../browser', true, /test$/); +testsContext.keys().forEach(testsContext); + +const testsContextCommon = require.context('../common', true, /test$/); +testsContextCommon.keys().forEach(testsContextCommon); + +const srcContext = require.context('.', true, /src$/); +srcContext.keys().forEach(srcContext); diff --git a/packages/opentelemetry-exporter-zipkin/test/browser/zipkin.test.ts b/packages/opentelemetry-exporter-zipkin/test/browser/zipkin.test.ts new file mode 100644 index 0000000000..c47e69fa57 --- /dev/null +++ b/packages/opentelemetry-exporter-zipkin/test/browser/zipkin.test.ts @@ -0,0 +1,157 @@ +/* + * 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 { NoopLogger } from '@opentelemetry/core'; +import { ReadableSpan } from '@opentelemetry/tracing'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { ZipkinExporter } from '../../src'; +import * as zipkinTypes from '../../src/types'; +import { + ensureHeadersContain, + ensureSpanIsCorrect, + mockedReadableSpan, +} from '../helper'; + +const sendBeacon = navigator.sendBeacon; + +describe('Zipkin Exporter - web', () => { + let zipkinExporter: ZipkinExporter; + let zipkinConfig: zipkinTypes.ExporterConfig = {}; + let spySend: sinon.SinonSpy; + let spyBeacon: sinon.SinonSpy; + let spans: ReadableSpan[]; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + spySend = sandbox.stub(XMLHttpRequest.prototype, 'send'); + spyBeacon = sandbox.stub(navigator, 'sendBeacon'); + spans = []; + spans.push(Object.assign({}, mockedReadableSpan)); + }); + + afterEach(() => { + sandbox.restore(); + navigator.sendBeacon = sendBeacon; + }); + + describe('export', () => { + describe('when "sendBeacon" is available', () => { + beforeEach(() => { + zipkinExporter = new ZipkinExporter(zipkinConfig); + }); + + it('should successfully send the spans using sendBeacon', done => { + zipkinExporter.export(spans, () => {}); + + setTimeout(() => { + const args = spyBeacon.args[0]; + const body = args[1]; + const json = JSON.parse(body) as any; + ensureSpanIsCorrect(json[0]); + assert.strictEqual(spyBeacon.callCount, 1); + assert.strictEqual(spySend.callCount, 0); + + done(); + }); + }); + }); + + describe('when "sendBeacon" is NOT available', () => { + let server: any; + beforeEach(() => { + (window.navigator as any).sendBeacon = false; + zipkinExporter = new ZipkinExporter(zipkinConfig); + server = sinon.fakeServer.create(); + }); + afterEach(() => { + server.restore(); + }); + + it('should successfully send the spans using XMLHttpRequest', done => { + zipkinExporter.export(spans, () => {}); + + setTimeout(() => { + const request = server.requests[0]; + const body = request.requestBody; + const json = JSON.parse(body) as any; + ensureSpanIsCorrect(json[0]); + + done(); + }); + }); + }); + }); + + describe('export with custom headers', () => { + let server: any; + const customHeaders = { + foo: 'bar', + bar: 'baz', + }; + + beforeEach(() => { + zipkinConfig = { + logger: new NoopLogger(), + headers: customHeaders, + }; + server = sinon.fakeServer.create(); + }); + + afterEach(() => { + server.restore(); + }); + + describe('when "sendBeacon" is available', () => { + beforeEach(() => { + zipkinExporter = new ZipkinExporter(zipkinConfig); + }); + it('should successfully send custom headers using XMLHTTPRequest', done => { + zipkinExporter.export(spans, () => {}); + + setTimeout(() => { + const [{ requestHeaders }] = server.requests; + + ensureHeadersContain(requestHeaders, customHeaders); + assert.strictEqual(spyBeacon.callCount, 0); + + done(); + }); + }); + }); + + describe('when "sendBeacon" is NOT available', () => { + beforeEach(() => { + (window.navigator as any).sendBeacon = false; + zipkinExporter = new ZipkinExporter(zipkinConfig); + }); + + it('should successfully send custom headers using XMLHTTPRequest', done => { + zipkinExporter.export(spans, () => {}); + + setTimeout(() => { + const [{ requestHeaders }] = server.requests; + + ensureHeadersContain(requestHeaders, customHeaders); + assert.strictEqual(spyBeacon.callCount, 0); + + done(); + }); + }); + }); + }); +}); diff --git a/packages/opentelemetry-exporter-zipkin/test/transform.test.ts b/packages/opentelemetry-exporter-zipkin/test/common/transform.test.ts similarity index 95% rename from packages/opentelemetry-exporter-zipkin/test/transform.test.ts rename to packages/opentelemetry-exporter-zipkin/test/common/transform.test.ts index 05cbbbfd0d..ad4472066f 100644 --- a/packages/opentelemetry-exporter-zipkin/test/transform.test.ts +++ b/packages/opentelemetry-exporter-zipkin/test/common/transform.test.ts @@ -29,14 +29,16 @@ import { _toZipkinAnnotations, statusCodeTagName, statusDescriptionTagName, -} from '../src/transform'; -import * as zipkinTypes from '../src/types'; -import { Resource } from '@opentelemetry/resources'; - +} from '../../src/transform'; +import * as zipkinTypes from '../../src/types'; +import { Resource, TELEMETRY_SDK_RESOURCE } from '@opentelemetry/resources'; const logger = new NoopLogger(); const tracer = new BasicTracerProvider({ logger, }).getTracer('default'); + +const language = tracer.resource.attributes[TELEMETRY_SDK_RESOURCE.LANGUAGE]; + const parentId = '5c1c63257de34c67'; const spanContext: api.SpanContext = { traceId: 'd4cda95b652f4a1592b449d5929fda1b', @@ -94,7 +96,7 @@ describe('transform', () => { key1: 'value1', key2: 'value2', [statusCodeTagName]: 'OK', - 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.language': language, 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': VERSION, }, @@ -131,7 +133,7 @@ describe('transform', () => { parentId: undefined, tags: { [statusCodeTagName]: 'OK', - 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.language': language, 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': VERSION, }, @@ -173,7 +175,7 @@ describe('transform', () => { parentId: undefined, tags: { [statusCodeTagName]: 'OK', - 'telemetry.sdk.language': 'nodejs', + 'telemetry.sdk.language': language, 'telemetry.sdk.name': 'opentelemetry', 'telemetry.sdk.version': VERSION, }, diff --git a/packages/opentelemetry-exporter-zipkin/test/helper.ts b/packages/opentelemetry-exporter-zipkin/test/helper.ts new file mode 100644 index 0000000000..8fa1cfe1e4 --- /dev/null +++ b/packages/opentelemetry-exporter-zipkin/test/helper.ts @@ -0,0 +1,78 @@ +/* + * 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 { TraceFlags } from '@opentelemetry/api'; +import { ReadableSpan } from '@opentelemetry/tracing'; +import { Resource } from '@opentelemetry/resources'; +import * as assert from 'assert'; +import { Span } from '../src/types'; + +export const mockedReadableSpan: ReadableSpan = { + name: 'documentFetch', + kind: 0, + spanContext: { + traceId: '1f1008dc8e270e85c40a0d7c3939b278', + spanId: '5e107261f64fa53e', + traceFlags: TraceFlags.SAMPLED, + }, + parentSpanId: '78a8915098864388', + startTime: [1574120165, 429803070], + endTime: [1574120165, 438688070], + ended: true, + status: { code: 0 }, + attributes: { component: 'foo' }, + links: [], + events: [], + duration: [0, 8885000], + resource: new Resource({ + service: 'ui', + version: 1, + cost: 112.12, + }), + instrumentationLibrary: { name: 'default', version: '0.0.1' }, +}; + +export function ensureHeadersContain( + actual: { [key: string]: string }, + expected: { [key: string]: string } +) { + Object.entries(expected).forEach(([k, v]) => { + assert.strictEqual( + v, + actual[k], + `Expected ${actual} to contain ${k}: ${v}` + ); + }); +} + +export function ensureSpanIsCorrect(span: Span) { + assert.deepStrictEqual(span, { + traceId: '1f1008dc8e270e85c40a0d7c3939b278', + parentId: '78a8915098864388', + name: 'documentFetch', + id: '5e107261f64fa53e', + timestamp: 1574120165429803, + duration: 8885, + localEndpoint: { serviceName: 'OpenTelemetry Service' }, + tags: { + component: 'foo', + 'ot.status_code': 'OK', + service: 'ui', + version: 1, + cost: 112.12, + }, + }); +} diff --git a/packages/opentelemetry-exporter-zipkin/test/zipkin.test.ts b/packages/opentelemetry-exporter-zipkin/test/node/zipkin.test.ts similarity index 95% rename from packages/opentelemetry-exporter-zipkin/test/zipkin.test.ts rename to packages/opentelemetry-exporter-zipkin/test/node/zipkin.test.ts index 7dd50b8381..803f88838d 100644 --- a/packages/opentelemetry-exporter-zipkin/test/zipkin.test.ts +++ b/packages/opentelemetry-exporter-zipkin/test/node/zipkin.test.ts @@ -24,9 +24,9 @@ import { } from '@opentelemetry/core'; import * as api from '@opentelemetry/api'; import { Resource } from '@opentelemetry/resources'; -import { ZipkinExporter } from '../src'; -import * as zipkinTypes from '../src/types'; -import { OT_REQUEST_HEADER } from '../src/utils'; +import { ZipkinExporter } from '../../src'; +import * as zipkinTypes from '../../src/types'; +import { OT_REQUEST_HEADER } from '../../src/utils'; import { TraceFlags } from '@opentelemetry/api'; import { SERVICE_RESOURCE } from '@opentelemetry/resources'; @@ -59,7 +59,7 @@ function getReadableSpan() { return readableSpan; } -describe('ZipkinExporter', () => { +describe('Zipkin Exporter - node', () => { describe('constructor', () => { it('should construct an exporter', () => { const exporter = new ZipkinExporter({ serviceName: 'my-service' }); @@ -98,21 +98,6 @@ describe('ZipkinExporter', () => { assert.ok(typeof exporter.export === 'function'); assert.ok(typeof exporter.shutdown === 'function'); }); - it('should construct an exporter with headers', () => { - const exporter = new ZipkinExporter({ - headers: { - foo: 'bar', - }, - }); - interface ExporterWithHeaders { - _reqOpts: { - headers: { [key: string]: string }; - }; - } - const exporterWithHeaders = (exporter as unknown) as ExporterWithHeaders; - - assert.ok(exporterWithHeaders._reqOpts.headers['foo'] === 'bar'); - }); }); describe('export', () => { @@ -510,8 +495,5 @@ describe('ZipkinExporter', () => { after(() => { nock.enableNetConnect(); }); - - // @todo: implement - it('should send by default'); }); });