From b58ad102c45dbefb0c86bc8beb8ef755a05f1a7e Mon Sep 17 00:00:00 2001 From: Bartlomiej Obecny Date: Wed, 4 Dec 2019 17:43:09 +0100 Subject: [PATCH] Collector exporter (#552) * chore: linting * feat(collector-exporter): new exporter for opentelemetry collector * chore: updating readme * chore: undo auto lint fix - which is wrong * chore: updates after comments * chore: renaming util to transform * chore: renaming types, last comments from review * chore: adding missing links * chore: fixes after comments * chore: fixes after comments * chore: fixes after comments * chore: updating jsdoc * chore: enabling attributes * chore: adding script to generate package version file * chore: naming * chore: adding todo * chore: updating types for link * chore: fixing typo * chore: removing unnecessary typing * chore: const for enum * chore: adding missing interface for message event * chore: adding timestamp example * chore: changes after review * chore: adding case when the exporter is shutdown but export is called * chore: adding missing header for request to prevent instrumentation --- examples/basic-tracer-node/README.md | 24 + .../docker/ot/collector-config.yaml | 18 + .../docker/ot/docker-compose.yaml | 19 + examples/basic-tracer-node/index.js | 7 +- examples/basic-tracer-node/multi_exporter.js | 7 +- examples/basic-tracer-node/package.json | 4 + examples/tracer-web/index.html | 2 +- examples/tracer-web/index.js | 2 + examples/tracer-web/package.json | 1 + packages/opentelemetry-core/package.json | 6 +- .../scripts/version-update.js | 47 ++ .../opentelemetry-core/src/common/time.ts | 58 +- .../opentelemetry-core/src/common/version.ts | 18 + packages/opentelemetry-core/src/index.ts | 1 + .../src/platform/browser/hex-to-base64.ts | 30 + .../src/platform/browser/index.ts | 1 + .../src/platform/node/hex-to-base64.ts | 31 ++ .../src/platform/node/index.ts | 1 + .../test/common/time.test.ts | 10 + .../test/platform/hex-to-base64.test.ts | 27 + .../.npmignore | 4 + .../opentelemetry-exporter-collector/LICENSE | 201 +++++++ .../README.md | 77 +++ .../karma.conf.js | 26 + .../package.json | 87 +++ .../scripts/version-update.js | 47 ++ .../src/CollectorExporter.ts | 134 +++++ .../src/index.ts | 17 + .../src/platform/browser/index.ts | 17 + .../src/platform/browser/sendSpans.ts | 150 +++++ .../src/platform/index.ts | 17 + .../src/platform/node/index.ts | 17 + .../src/platform/node/sendSpans.ts | 107 ++++ .../src/transform.ts | 218 ++++++++ .../src/types.ts | 520 ++++++++++++++++++ .../src/version.ts | 18 + .../test/browser/CollectorExporter.test.ts | 203 +++++++ .../test/browser/index-webpack.ts | 26 + .../test/common/CollectorExporter.test.ts | 174 ++++++ .../test/common/transform.test.ts | 152 +++++ .../test/helper.ts | 195 +++++++ .../test/node/CollectorExporter.test.ts | 149 +++++ .../tsconfig-release.json | 7 + .../tsconfig.json | 11 + .../tslint.json | 4 + 45 files changed, 2878 insertions(+), 14 deletions(-) create mode 100644 examples/basic-tracer-node/docker/ot/collector-config.yaml create mode 100644 examples/basic-tracer-node/docker/ot/docker-compose.yaml create mode 100644 packages/opentelemetry-core/scripts/version-update.js create mode 100644 packages/opentelemetry-core/src/common/version.ts create mode 100644 packages/opentelemetry-core/src/platform/browser/hex-to-base64.ts create mode 100644 packages/opentelemetry-core/src/platform/node/hex-to-base64.ts create mode 100644 packages/opentelemetry-core/test/platform/hex-to-base64.test.ts create mode 100644 packages/opentelemetry-exporter-collector/.npmignore create mode 100644 packages/opentelemetry-exporter-collector/LICENSE create mode 100644 packages/opentelemetry-exporter-collector/README.md create mode 100644 packages/opentelemetry-exporter-collector/karma.conf.js create mode 100644 packages/opentelemetry-exporter-collector/package.json create mode 100644 packages/opentelemetry-exporter-collector/scripts/version-update.js create mode 100644 packages/opentelemetry-exporter-collector/src/CollectorExporter.ts create mode 100644 packages/opentelemetry-exporter-collector/src/index.ts create mode 100644 packages/opentelemetry-exporter-collector/src/platform/browser/index.ts create mode 100644 packages/opentelemetry-exporter-collector/src/platform/browser/sendSpans.ts create mode 100644 packages/opentelemetry-exporter-collector/src/platform/index.ts create mode 100644 packages/opentelemetry-exporter-collector/src/platform/node/index.ts create mode 100644 packages/opentelemetry-exporter-collector/src/platform/node/sendSpans.ts create mode 100644 packages/opentelemetry-exporter-collector/src/transform.ts create mode 100644 packages/opentelemetry-exporter-collector/src/types.ts create mode 100644 packages/opentelemetry-exporter-collector/src/version.ts create mode 100644 packages/opentelemetry-exporter-collector/test/browser/CollectorExporter.test.ts create mode 100644 packages/opentelemetry-exporter-collector/test/browser/index-webpack.ts create mode 100644 packages/opentelemetry-exporter-collector/test/common/CollectorExporter.test.ts create mode 100644 packages/opentelemetry-exporter-collector/test/common/transform.test.ts create mode 100644 packages/opentelemetry-exporter-collector/test/helper.ts create mode 100644 packages/opentelemetry-exporter-collector/test/node/CollectorExporter.test.ts create mode 100644 packages/opentelemetry-exporter-collector/tsconfig-release.json create mode 100644 packages/opentelemetry-exporter-collector/tsconfig.json create mode 100644 packages/opentelemetry-exporter-collector/tslint.json diff --git a/examples/basic-tracer-node/README.md b/examples/basic-tracer-node/README.md index d7e0feb9ea..ce75a56078 100644 --- a/examples/basic-tracer-node/README.md +++ b/examples/basic-tracer-node/README.md @@ -13,6 +13,9 @@ $ npm install Setup [Zipkin Tracing](https://zipkin.io/pages/quickstart.html) or Setup [Jaeger Tracing](https://www.jaegertracing.io/docs/latest/getting-started/#all-in-one) +or +Setup [Collector Exporter](https://github.com/open-telemetry/opentelemetry-exporter-collector) + ## Run the Application @@ -57,6 +60,24 @@ Click on the trace to view its details.

+### Collector Exporter +You can use the [opentelemetry-collector][opentelemetry-collector-url] docker container. +For that please make sure you have [docker](https://docs.docker.com/) installed + - Run the docker container + ```sh + $ # from this directory + $ # open telemetry + $ npm run collector:docker:ot + $ # at any time you can stop it + $ npm run collector:docker:stop + ``` + +#### Collector Exporter - Zipkin UI +The [opentelemetry-collector][opentelemetry-collector-url] +docker container is using [Zipkin Exporter](#zipkin). +You can define more exporters without changing the instrumented code. +To use default [Zipkin Exporter](#zipkin) please follow the section [Zipkin UI](#zipkin-ui) only + ### Export to multiple exporters - Run the sample @@ -75,3 +96,6 @@ Click on the trace to view its details. ## LICENSE Apache License 2.0 + + +[opentelemetry-collector-url]: https://github.com/open-telemetry/opentelemetry-exporter-collector diff --git a/examples/basic-tracer-node/docker/ot/collector-config.yaml b/examples/basic-tracer-node/docker/ot/collector-config.yaml new file mode 100644 index 0000000000..b4bc9cb9fc --- /dev/null +++ b/examples/basic-tracer-node/docker/ot/collector-config.yaml @@ -0,0 +1,18 @@ +receivers: + opencensus: + endpoint: 0.0.0.0:55678 + +exporters: + zipkin: + url: "http://zipkin-all-in-one:9411/api/v2/spans" + +processors: + batch: + queued_retry: + +service: + pipelines: + traces: + receivers: [opencensus] + exporters: [zipkin] + processors: [batch, queued_retry] diff --git a/examples/basic-tracer-node/docker/ot/docker-compose.yaml b/examples/basic-tracer-node/docker/ot/docker-compose.yaml new file mode 100644 index 0000000000..3e486bfb05 --- /dev/null +++ b/examples/basic-tracer-node/docker/ot/docker-compose.yaml @@ -0,0 +1,19 @@ +version: "2" +services: + + # Collector + collector: + image: otelcol:latest + command: ["--config=/conf/collector-config.yaml", "--log-level=DEBUG"] + volumes: + - ./collector-config.yaml:/conf/collector-config.yaml + ports: + - "55678:55678" + depends_on: + - zipkin-all-in-one + + # Zipkin + zipkin-all-in-one: + image: openzipkin/zipkin:latest + ports: + - "9411:9411" diff --git a/examples/basic-tracer-node/index.js b/examples/basic-tracer-node/index.js index 19b2a7b881..61bbe33b54 100644 --- a/examples/basic-tracer-node/index.js +++ b/examples/basic-tracer-node/index.js @@ -2,10 +2,11 @@ const opentelemetry = require('@opentelemetry/core'); const { BasicTracer, SimpleSpanProcessor } = require('@opentelemetry/tracing'); const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin'); +const { CollectorExporter } = require('@opentelemetry/exporter-collector'); const options = { serviceName: 'basic-service' -} +}; // Initialize an exporter depending on how we were started let exporter; @@ -13,8 +14,10 @@ let exporter; const EXPORTER = process.env.EXPORTER || ''; if (EXPORTER.toLowerCase().startsWith('z')) { exporter = new ZipkinExporter(options); -} else { +} else if (EXPORTER.toLowerCase().startsWith('j')) { exporter = new JaegerExporter(options); +} else { + exporter = new CollectorExporter(options); } const tracer = new BasicTracer(); diff --git a/examples/basic-tracer-node/multi_exporter.js b/examples/basic-tracer-node/multi_exporter.js index dc4c85474b..e5920a7e6b 100644 --- a/examples/basic-tracer-node/multi_exporter.js +++ b/examples/basic-tracer-node/multi_exporter.js @@ -2,6 +2,7 @@ const opentelemetry = require('@opentelemetry/core'); const { BasicTracer, BatchSpanProcessor, SimpleSpanProcessor } = require('@opentelemetry/tracing'); const { JaegerExporter } = require('@opentelemetry/exporter-jaeger'); const { ZipkinExporter } = require('@opentelemetry/exporter-zipkin'); +const { CollectorExporter } = require('@opentelemetry/exporter-collector'); const tracer = new BasicTracer(); @@ -10,7 +11,8 @@ const jaegerExporter = new JaegerExporter({ serviceName: 'basic-service', // The default flush interval is 5 seconds. flushInterval: 2000 -}) +}); +const collectorExporter = new CollectorExporter({serviceName: 'basic-service'}); // It is recommended to use this BatchSpanProcessor for better performance // and optimization, especially in production. @@ -22,6 +24,8 @@ tracer.addSpanProcessor(new BatchSpanProcessor(zipkinExporter, { // it's internal client already handles the spans with batching logic. tracer.addSpanProcessor(new SimpleSpanProcessor(jaegerExporter)); +tracer.addSpanProcessor(new SimpleSpanProcessor(collectorExporter)); + // Initialize the OpenTelemetry APIs to use the BasicTracer bindings opentelemetry.initGlobalTracer(tracer); @@ -36,6 +40,7 @@ span.end(); // flush and close the connection. zipkinExporter.shutdown(); jaegerExporter.shutdown(); +collectorExporter.shutdown(); function doWork(parent) { // Start another span. In this example, the main method already started a diff --git a/examples/basic-tracer-node/package.json b/examples/basic-tracer-node/package.json index 77417ab385..05063bd87a 100644 --- a/examples/basic-tracer-node/package.json +++ b/examples/basic-tracer-node/package.json @@ -7,6 +7,9 @@ "scripts": { "zipkin:basic": "cross-env EXPORTER=zipkin node ./index.js", "jaeger:basic": "cross-env EXPORTER=jaeger node ./index.js", + "collector:basic": "cross-env EXPORTER=collector node ./index.js", + "collector:docker:ot": "cd ./docker/ot && docker-compose down && docker-compose up", + "collector:docker:stop": "cd ./docker/ot && docker-compose down", "multi_exporter": "node ./multi_exporter.js" }, "repository": { @@ -28,6 +31,7 @@ }, "dependencies": { "@opentelemetry/core": "^0.2.0", + "@opentelemetry/exporter-collector": "^0.2.0", "@opentelemetry/exporter-jaeger": "^0.2.0", "@opentelemetry/exporter-zipkin": "^0.2.0", "@opentelemetry/tracing": "^0.2.0" diff --git a/examples/tracer-web/index.html b/examples/tracer-web/index.html index 947630827c..395281f3e8 100644 --- a/examples/tracer-web/index.html +++ b/examples/tracer-web/index.html @@ -21,7 +21,7 @@ - Example of using Web Tracer with document load plugin and console exporter + Example of using Web Tracer with document load plugin with console exporter and collector exporter
diff --git a/examples/tracer-web/index.js b/examples/tracer-web/index.js index 0e5e9956e3..c08a3c0f39 100644 --- a/examples/tracer-web/index.js +++ b/examples/tracer-web/index.js @@ -2,6 +2,7 @@ import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing import { WebTracer } from '@opentelemetry/web'; import { DocumentLoad } from '@opentelemetry/plugin-document-load'; import { ZoneScopeManager } from '@opentelemetry/scope-zone'; +import { CollectorExporter } from '@opentelemetry/exporter-collector' const webTracer = new WebTracer({ plugins: [ @@ -17,6 +18,7 @@ const webTracerWithZone = new WebTracer({ ] }); webTracerWithZone.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); +webTracerWithZone.addSpanProcessor(new SimpleSpanProcessor(new CollectorExporter())); console.log('Current span is window', webTracerWithZone.getCurrentSpan() === window); diff --git a/examples/tracer-web/package.json b/examples/tracer-web/package.json index 35c98c8240..13f561b121 100644 --- a/examples/tracer-web/package.json +++ b/examples/tracer-web/package.json @@ -34,6 +34,7 @@ "webpack-merge": "^4.2.2" }, "dependencies": { + "@opentelemetry/exporter-collector": "^0.2.0", "@opentelemetry/plugin-document-load": "^0.2.0", "@opentelemetry/scope-zone": "^0.2.0", "@opentelemetry/tracing": "^0.2.0", diff --git a/packages/opentelemetry-core/package.json b/packages/opentelemetry-core/package.json index 8b56e65d9c..915aa32cb1 100644 --- a/packages/opentelemetry-core/package.json +++ b/packages/opentelemetry-core/package.json @@ -20,9 +20,11 @@ "clean": "rimraf build/*", "check": "gts check", "precompile": "tsc --version", - "compile": "tsc -p .", + "compile": "npm run version:update && tsc -p .", "fix": "gts fix", - "prepare": "npm run compile" + "prepare": "npm run compile", + "version:update": "node scripts/version-update.js", + "watch": "tsc -w" }, "keywords": [ "opentelemetry", diff --git a/packages/opentelemetry-core/scripts/version-update.js b/packages/opentelemetry-core/scripts/version-update.js new file mode 100644 index 0000000000..3240a96b4b --- /dev/null +++ b/packages/opentelemetry-core/scripts/version-update.js @@ -0,0 +1,47 @@ +/*! + * Copyright 2019, 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 license = +`/*! + * Copyright 2019, 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 fs = require('fs'); +const path = require('path'); + +const appRoot = path.resolve(__dirname); +const fileUrl = path.resolve(`${appRoot}/../src/common/version.ts`); +const packageJsonUrl = path.resolve(`${appRoot}/../package.json`); +const pjson = require(packageJsonUrl); +const content = ` +// this is autogenerated file, see scripts/version-update.js +export const VERSION = '${pjson.version}'; +`; + +fs.writeFileSync(fileUrl, `${license}${content}`); diff --git a/packages/opentelemetry-core/src/common/time.ts b/packages/opentelemetry-core/src/common/time.ts index fcc564692e..861c9efc44 100644 --- a/packages/opentelemetry-core/src/common/time.ts +++ b/packages/opentelemetry-core/src/common/time.ts @@ -21,7 +21,10 @@ import { TimeOriginLegacy } from './types'; const NANOSECOND_DIGITS = 9; const SECOND_TO_NANOSECONDS = Math.pow(10, NANOSECOND_DIGITS); -// Converts a number to HrTime +/** + * Converts a number to HrTime + * @param epochMillis + */ function numberToHrtime(epochMillis: number): types.HrTime { const epochSeconds = epochMillis / 1000; // Decimals only. @@ -42,7 +45,10 @@ function getTimeOrigin(): number { return timeOrigin; } -// Returns an hrtime calculated via performance component. +/** + * Returns an hrtime calculated via performance component. + * @param performanceNow + */ export function hrTime(performanceNow?: number): types.HrTime { const timeOrigin = numberToHrtime(getTimeOrigin()); const now = numberToHrtime( @@ -61,7 +67,11 @@ export function hrTime(performanceNow?: number): types.HrTime { return [seconds, nanos]; } -// Converts a TimeInput to an HrTime, defaults to _hrtime(). +/** + * + * Converts a TimeInput to an HrTime, defaults to _hrtime(). + * @param time + */ export function timeInputToHrTime(time: types.TimeInput): types.HrTime { // process.hrtime if (isTimeInputHrTime(time)) { @@ -81,7 +91,11 @@ export function timeInputToHrTime(time: types.TimeInput): types.HrTime { } } -// Returns a duration of two hrTime. +/** + * Returns a duration of two hrTime. + * @param startTime + * @param endTime + */ export function hrTimeDuration( startTime: types.HrTime, endTime: types.HrTime @@ -99,21 +113,46 @@ export function hrTimeDuration( return [seconds, nanos]; } -// Convert hrTime to nanoseconds. +/** + * Convert hrTime to timestamp, for example "2019-05-14T17:00:00.000123456Z" + * @param hrTime + */ +export function hrTimeToTimeStamp(hrTime: types.HrTime): string { + const precision = NANOSECOND_DIGITS; + const tmp = `${'0'.repeat(precision)}${hrTime[1]}Z`; + const nanoString = tmp.substr(tmp.length - precision - 1); + const date = new Date(hrTime[0] * 1000).toISOString(); + return date.replace('000Z', nanoString); +} + +/** + * Convert hrTime to nanoseconds. + * @param hrTime + */ export function hrTimeToNanoseconds(hrTime: types.HrTime): number { return hrTime[0] * SECOND_TO_NANOSECONDS + hrTime[1]; } -// Convert hrTime to milliseconds. +/** + * Convert hrTime to milliseconds. + * @param hrTime + */ export function hrTimeToMilliseconds(hrTime: types.HrTime): number { return Math.round(hrTime[0] * 1e3 + hrTime[1] / 1e6); } -// Convert hrTime to microseconds. +/** + * Convert hrTime to microseconds. + * @param hrTime + */ export function hrTimeToMicroseconds(hrTime: types.HrTime): number { return Math.round(hrTime[0] * 1e6 + hrTime[1] / 1e3); } +/** + * check if time is HrTime + * @param value + */ export function isTimeInputHrTime(value: unknown) { return ( Array.isArray(value) && @@ -123,7 +162,10 @@ export function isTimeInputHrTime(value: unknown) { ); } -// check if input value is a correct types.TimeInput +/** + * check if input value is a correct types.TimeInput + * @param value + */ export function isTimeInput(value: unknown) { return ( isTimeInputHrTime(value) || diff --git a/packages/opentelemetry-core/src/common/version.ts b/packages/opentelemetry-core/src/common/version.ts new file mode 100644 index 0000000000..5e6eb85dbe --- /dev/null +++ b/packages/opentelemetry-core/src/common/version.ts @@ -0,0 +1,18 @@ +/*! + * Copyright 2019, 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. + */ + +// this is autogenerated file, see scripts/version-update.js +export const VERSION = '0.2.0'; diff --git a/packages/opentelemetry-core/src/index.ts b/packages/opentelemetry-core/src/index.ts index 581d9ab055..236ad9d2cd 100644 --- a/packages/opentelemetry-core/src/index.ts +++ b/packages/opentelemetry-core/src/index.ts @@ -18,6 +18,7 @@ export * from './common/ConsoleLogger'; export * from './common/NoopLogger'; export * from './common/time'; export * from './common/types'; +export * from './common/version'; export * from './context/propagation/B3Format'; export * from './context/propagation/BinaryTraceContext'; export * from './context/propagation/HttpTraceContext'; diff --git a/packages/opentelemetry-core/src/platform/browser/hex-to-base64.ts b/packages/opentelemetry-core/src/platform/browser/hex-to-base64.ts new file mode 100644 index 0000000000..d0441b7772 --- /dev/null +++ b/packages/opentelemetry-core/src/platform/browser/hex-to-base64.ts @@ -0,0 +1,30 @@ +/*! + * Copyright 2019, 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. + */ + +/** + * converts id string into base64 + * @param hexStr - id of span + */ +export function hexToBase64(hexStr: string): string { + const hexStrLen = hexStr.length; + let hexAsciiCharsStr = ''; + for (let i = 0; i < hexStrLen; i += 2) { + const hexPair = hexStr.substring(i, i + 2); + const hexVal = parseInt(hexPair, 16); + hexAsciiCharsStr += String.fromCharCode(hexVal); + } + return btoa(hexAsciiCharsStr); +} diff --git a/packages/opentelemetry-core/src/platform/browser/index.ts b/packages/opentelemetry-core/src/platform/browser/index.ts index 8b1b9afe53..4668bb35aa 100644 --- a/packages/opentelemetry-core/src/platform/browser/index.ts +++ b/packages/opentelemetry-core/src/platform/browser/index.ts @@ -17,3 +17,4 @@ export * from './id'; export * from './performance'; export * from './timer-util'; +export * from './hex-to-base64'; diff --git a/packages/opentelemetry-core/src/platform/node/hex-to-base64.ts b/packages/opentelemetry-core/src/platform/node/hex-to-base64.ts new file mode 100644 index 0000000000..1ed62f9cf9 --- /dev/null +++ b/packages/opentelemetry-core/src/platform/node/hex-to-base64.ts @@ -0,0 +1,31 @@ +/*! + * Copyright 2019, 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. + */ + +/** + * converts id string into base64 + * @param hexStr - id of span + */ +export function hexToBase64(hexStr: string): string { + const hexStrLen = hexStr.length; + let hexAsciiCharsStr = ''; + for (let i = 0; i < hexStrLen; i += 2) { + const hexPair = hexStr.substring(i, i + 2); + const hexVal = parseInt(hexPair, 16); + hexAsciiCharsStr += String.fromCharCode(hexVal); + } + + return Buffer.from(hexAsciiCharsStr, 'ascii').toString('base64'); +} diff --git a/packages/opentelemetry-core/src/platform/node/index.ts b/packages/opentelemetry-core/src/platform/node/index.ts index 8b1b9afe53..4668bb35aa 100644 --- a/packages/opentelemetry-core/src/platform/node/index.ts +++ b/packages/opentelemetry-core/src/platform/node/index.ts @@ -17,3 +17,4 @@ export * from './id'; export * from './performance'; export * from './timer-util'; +export * from './hex-to-base64'; diff --git a/packages/opentelemetry-core/test/common/time.test.ts b/packages/opentelemetry-core/test/common/time.test.ts index 4eb82a9b78..a65af498fc 100644 --- a/packages/opentelemetry-core/test/common/time.test.ts +++ b/packages/opentelemetry-core/test/common/time.test.ts @@ -25,6 +25,7 @@ import { hrTimeToNanoseconds, hrTimeToMilliseconds, hrTimeToMicroseconds, + hrTimeToTimeStamp, isTimeInput, } from '../../src/common/time'; @@ -156,6 +157,15 @@ describe('time', () => { }); }); + describe('#hrTimeToTimeStamp', () => { + it('should return timestamp', () => { + const time: types.HrTime = [1573513121, 123456]; + + const output = hrTimeToTimeStamp(time); + assert.deepStrictEqual(output, '2019-11-11T22:58:41.000123456Z'); + }); + }); + describe('#hrTimeToNanoseconds', () => { it('should return nanoseconds', () => { const output = hrTimeToNanoseconds([1, 200000000]); diff --git a/packages/opentelemetry-core/test/platform/hex-to-base64.test.ts b/packages/opentelemetry-core/test/platform/hex-to-base64.test.ts new file mode 100644 index 0000000000..1165ef7b95 --- /dev/null +++ b/packages/opentelemetry-core/test/platform/hex-to-base64.test.ts @@ -0,0 +1,27 @@ +/*! + * Copyright 2019, 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 { hexToBase64 } from '../../src/platform'; + +describe('hexToBase64', () => { + it('convert hex to base64', () => { + const id1 = '7deb739e02e44ef2'; + const id2 = '46cef837b919a16ff26e608c8cf42c80'; + assert.strictEqual(hexToBase64(id1), 'fetzngLkTvI='); + assert.strictEqual(hexToBase64(id2), 'Rs74N7kZoW/ybmCMjPQsgA=='); + }); +}); diff --git a/packages/opentelemetry-exporter-collector/.npmignore b/packages/opentelemetry-exporter-collector/.npmignore new file mode 100644 index 0000000000..9505ba9450 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/.npmignore @@ -0,0 +1,4 @@ +/bin +/coverage +/doc +/test diff --git a/packages/opentelemetry-exporter-collector/LICENSE b/packages/opentelemetry-exporter-collector/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/packages/opentelemetry-exporter-collector/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/packages/opentelemetry-exporter-collector/README.md b/packages/opentelemetry-exporter-collector/README.md new file mode 100644 index 0000000000..25574ea689 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/README.md @@ -0,0 +1,77 @@ +# OpenTelemetry Collector Exporter for web and node +[![Gitter chat][gitter-image]][gitter-url] +[![NPM Published Version][npm-img]][npm-url] +[![dependencies][dependencies-image]][dependencies-url] +[![devDependencies][devDependencies-image]][devDependencies-url] +[![Apache License][license-image]][license-image] + +This module provides exporter for web and node to be used with [opentelemetry-collector][opentelemetry-collector-url]. + +## Installation + +```bash +npm install --save @opentelemetry/exporter-collector +``` + +## Usage in Web +```js +import * as opentelemetry from '@opentelemetry/core'; +import { SimpleSpanProcessor } from '@opentelemetry/tracing'; +import { WebTracer } from '@opentelemetry/web'; +import { CollectorExporter } from '@opentelemetry/exporter-collector' + +const collectorOptions = { + url: '' // url is optional and can be omitted - default is http://localhost:55678/v1/trace +}; + +const tracer = new WebTracer(); +const exporter = new CollectorExporter(collectorOptions); +tracer.addSpanProcessor(new SimpleSpanProcessor(exporter)); + +opentelemetry.initGlobalTracer(tracer); + +``` + +## Usage in Node +```js +const opentelemetry = require('@opentelemetry/core'); +const { BasicTracer, SimpleSpanProcessor } = require('@opentelemetry/tracing'); +const { CollectorExporter } = require('@opentelemetry/exporter-collector'); + +const collectorOptions = { + url: '' // url is optional and can be omitted - default is http://localhost:55678/v1/trace +}; + +const tracer = new BasicTracer(); +const exporter = new CollectorExporter(collectorOptions); +tracer.addSpanProcessor(new SimpleSpanProcessor(exporter)); + +opentelemetry.initGlobalTracer(tracer); + +``` + +## Running opentelemetry-collector locally to see the traces +1. Go to examples/basic-tracer-node +2. run `npm run collector:docker:ot` +3. Open page at `http://localhost:9411/zipkin/` to observe the traces + +## Useful links +- For more information on OpenTelemetry, visit: +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us on [gitter][gitter-url] + +## License + +Apache 2.0 - See [LICENSE][license-url] for more information. + +[gitter-image]: https://badges.gitter.im/open-telemetry/opentelemetry-js.svg +[gitter-url]: https://gitter.im/open-telemetry/opentelemetry-node?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge +[license-url]: https://github.com/open-telemetry/opentelemetry-js/blob/master/LICENSE +[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat +[dependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/status.svg?path=packages/opentelemetry-exporter-collector +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-exporter-collector +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-exporter-collector +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-exporter-collector&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/exporter-collector +[npm-img]: https://badge.fury.io/js/%40opentelemetry%exporter-collector.svg +[opentelemetry-collector-url]: https://github.com/open-telemetry/opentelemetry-collector diff --git a/packages/opentelemetry-exporter-collector/karma.conf.js b/packages/opentelemetry-exporter-collector/karma.conf.js new file mode 100644 index 0000000000..86965a0095 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/karma.conf.js @@ -0,0 +1,26 @@ +/*! + * Copyright 2019, 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-collector/package.json b/packages/opentelemetry-exporter-collector/package.json new file mode 100644 index 0000000000..06e4eb8a51 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/package.json @@ -0,0 +1,87 @@ +{ + "name": "@opentelemetry/exporter-collector", + "version": "0.2.0", + "description": "OpenTelemetry Collector Exporter allows user to send collected traces to the OpenTelemetry Collector", + "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": { + "check": "gts check", + "clean": "rimraf build/*", + "codecov:browser": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "precompile": "tsc --version", + "compile": "npm run version:update && tsc -p .", + "fix": "gts fix", + "prepare": "npm run compile", + "tdd": "yarn test -- --watch-extensions ts --watch", + "tdd:browser": "karma start", + "test": "nyc ts-mocha -p tsconfig.json 'test/**/*.ts' --exclude 'test/browser/**/*.ts'", + "test:browser": "nyc karma start --single-run", + "version:update": "node scripts/version-update.js", + "watch": "tsc -w" + }, + "keywords": [ + "opentelemetry", + "nodejs", + "browser", + "tracing", + "profiling", + "metrics", + "stats" + ], + "author": "OpenTelemetry Authors", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + }, + "files": [ + "build/src/**/*.js", + "build/src/**/*.d.ts", + "doc", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@babel/core": "^7.6.0", + "@types/mocha": "^5.2.5", + "@types/node": "^12.6.8", + "@types/sinon": "^7.0.13", + "@types/webpack-env": "1.13.9", + "babel-loader": "^8.0.6", + "codecov": "^3.1.0", + "gts": "^1.0.0", + "istanbul-instrumenter-loader": "^3.0.1", + "karma": "^4.4.1", + "karma-chrome-launcher": "^3.1.0", + "karma-coverage-istanbul-reporter": "^2.1.0", + "karma-mocha": "^1.3.0", + "karma-spec-reporter": "^0.0.32", + "karma-webpack": "^4.0.2", + "mocha": "^6.1.0", + "nyc": "^14.1.1", + "rimraf": "^3.0.0", + "sinon": "^7.5.0", + "ts-loader": "^6.0.4", + "ts-mocha": "^6.0.0", + "ts-node": "^8.0.0", + "tslint-consistent-codestyle": "^1.16.0", + "tslint-microsoft-contrib": "^6.2.0", + "typescript": "3.6.4", + "webpack": "^4.35.2", + "webpack-cli": "^3.3.9", + "webpack-merge": "^4.2.2" + }, + "dependencies": { + "@opentelemetry/base": "^0.2.0", + "@opentelemetry/core": "^0.2.0", + "@opentelemetry/tracing": "^0.2.0", + "@opentelemetry/types": "^0.2.0" + } +} diff --git a/packages/opentelemetry-exporter-collector/scripts/version-update.js b/packages/opentelemetry-exporter-collector/scripts/version-update.js new file mode 100644 index 0000000000..2bb382ce6d --- /dev/null +++ b/packages/opentelemetry-exporter-collector/scripts/version-update.js @@ -0,0 +1,47 @@ +/*! + * Copyright 2019, 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 license = +`/*! + * Copyright 2019, 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 fs = require('fs'); +const path = require('path'); + +const appRoot = path.resolve(__dirname); +const fileUrl = path.resolve(`${appRoot}/../src/version.ts`); +const packageJsonUrl = path.resolve(`${appRoot}/../package.json`); +const pjson = require(packageJsonUrl); +const content = ` +// this is autogenerated file, see scripts/version-update.js +export const VERSION = '${pjson.version}'; +`; + +fs.writeFileSync(fileUrl, `${license}${content}`); diff --git a/packages/opentelemetry-exporter-collector/src/CollectorExporter.ts b/packages/opentelemetry-exporter-collector/src/CollectorExporter.ts new file mode 100644 index 0000000000..c7fff44901 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/CollectorExporter.ts @@ -0,0 +1,134 @@ +/*! + * Copyright 2019, 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 { ExportResult } from '@opentelemetry/base'; +import { NoopLogger } from '@opentelemetry/core'; +import { ReadableSpan, SpanExporter } from '@opentelemetry/tracing'; +import { Attributes, Logger } from '@opentelemetry/types'; +import * as collectorTypes from './types'; +import { toCollectorSpan } from './transform'; +import { onInit, onShutdown, sendSpans } from './platform/index'; + +/** + * Collector Exporter Config + */ +export interface CollectorExporterConfig { + hostName?: string; + logger?: Logger; + serviceName?: string; + attributes?: Attributes; + url?: string; +} + +const DEFAULT_SERVICE_NAME = 'collector-exporter'; +const DEFAULT_COLLECTOR_URL = 'http://localhost:55678/v1/trace'; + +/** + * Collector Exporter + */ +export class CollectorExporter implements SpanExporter { + readonly serviceName: string; + readonly url: string; + readonly logger: Logger; + readonly hostName: string | undefined; + readonly attributes?: Attributes; + private _isShutdown: boolean = false; + + /** + * @param config + */ + constructor(config: CollectorExporterConfig = {}) { + this.serviceName = config.serviceName || DEFAULT_SERVICE_NAME; + this.url = config.url || DEFAULT_COLLECTOR_URL; + if (typeof config.hostName === 'string') { + this.hostName = config.hostName; + } + + this.attributes = config.attributes; + + this.logger = config.logger || new NoopLogger(); + + this.shutdown = this.shutdown.bind(this); + + // platform dependent + onInit(this.shutdown); + } + + /** + * Export spans. + * @param spans + * @param resultCallback + */ + export( + spans: ReadableSpan[], + resultCallback: (result: ExportResult) => void + ) { + if (this._isShutdown) { + 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); + } + }); + } + + 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); + + // 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); + } catch (e) { + reject(e); + } + }); + } + + /** + * Shutdown the exporter. + */ + shutdown(): void { + if (this._isShutdown) { + this.logger.debug('shutdown already started'); + return; + } + this._isShutdown = true; + this.logger.debug('shutdown started'); + + // platform dependent + onShutdown(this.shutdown); + + // @TODO get spans from span processor (batch) + this._exportSpans([]) + .then(() => { + this.logger.debug('shutdown completed'); + }) + .catch(() => {}); + } +} diff --git a/packages/opentelemetry-exporter-collector/src/index.ts b/packages/opentelemetry-exporter-collector/src/index.ts new file mode 100644 index 0000000000..df00fee615 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/index.ts @@ -0,0 +1,17 @@ +/*! + * Copyright 2019, 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 './CollectorExporter'; diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/index.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/index.ts new file mode 100644 index 0000000000..e2ba0ac5a4 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/index.ts @@ -0,0 +1,17 @@ +/*! + * Copyright 2019, 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 './sendSpans'; diff --git a/packages/opentelemetry-exporter-collector/src/platform/browser/sendSpans.ts b/packages/opentelemetry-exporter-collector/src/platform/browser/sendSpans.ts new file mode 100644 index 0000000000..f7932a2afb --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/browser/sendSpans.ts @@ -0,0 +1,150 @@ +/*! + * Copyright 2019, 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 { Logger } from '@opentelemetry/types'; +import { CollectorExporter } from '../../CollectorExporter'; +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} + */ +export function onInit(shutdownF: EventListener) { + window.addEventListener('unload', shutdownF); +} + +/** + * function to be called once when {@link ExporterCollector} is shutdown + * @param shutdownF - shutdown method of {@link ExporterCollector} + */ +export function onShutdown(shutdownF: EventListener) { + window.removeEventListener('unload', shutdownF); +} + +/** + * function to send spans to the [opentelemetry collector]{@link https://github.com/open-telemetry/opentelemetry-collector} + * using the standard http/https node module + * @param spans + * @param onSuccess + * @param onError + * @param collectorExporter + */ +export function sendSpans( + spans: collectorTypes.Span[], + onSuccess: () => void, + onError: (status?: number) => 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: '', not implemented + spans, + }; + + const body = JSON.stringify(exportTraceServiceRequest); + + if (typeof navigator.sendBeacon === 'function') { + sendSpansWithBeacon( + body, + onSuccess, + onError, + collectorExporter.logger, + collectorExporter.url + ); + } else { + sendSpansWithXhr( + body, + onSuccess, + onError, + collectorExporter.logger, + collectorExporter.url + ); + } +} + +/** + * function to send spans using browser navigator.sendBeacon + * @param body + * @param onSuccess + * @param onError + * @param logger + * @param collectorUrl + */ +function sendSpansWithBeacon( + body: string, + onSuccess: () => void, + onError: (status?: number) => void, + logger: Logger, + collectorUrl: string +) { + if (navigator.sendBeacon(collectorUrl, body)) { + logger.debug('sendBeacon - can send', body); + onSuccess(); + } else { + logger.error('sendBeacon - cannot send', body); + onError(); + } +} + +/** + * function to send spans using browser XMLHttpRequest + * used when navigator.sendBeacon is not available + * @param body + * @param onSuccess + * @param onError + * @param logger + * @param collectorUrl + */ +function sendSpansWithXhr( + body: string, + onSuccess: () => void, + onError: (status?: number) => void, + logger: Logger, + collectorUrl: string +) { + const xhr = new XMLHttpRequest(); + xhr.open('POST', collectorUrl); + xhr.setRequestHeader(collectorTypes.OT_REQUEST_HEADER, '1'); + xhr.send(body); + + xhr.onreadystatechange = () => { + if (xhr.readyState === XMLHttpRequest.DONE) { + if (xhr.status >= 200 && xhr.status <= 299) { + logger.debug('xhr success', body); + onSuccess(); + } else { + logger.error('xhr error', xhr.status, body); + onError(xhr.status); + } + } + }; +} diff --git a/packages/opentelemetry-exporter-collector/src/platform/index.ts b/packages/opentelemetry-exporter-collector/src/platform/index.ts new file mode 100644 index 0000000000..16f6fd9be2 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/index.ts @@ -0,0 +1,17 @@ +/*! + * Copyright 2019, 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-collector/src/platform/node/index.ts b/packages/opentelemetry-exporter-collector/src/platform/node/index.ts new file mode 100644 index 0000000000..e2ba0ac5a4 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/node/index.ts @@ -0,0 +1,17 @@ +/*! + * Copyright 2019, 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 './sendSpans'; diff --git a/packages/opentelemetry-exporter-collector/src/platform/node/sendSpans.ts b/packages/opentelemetry-exporter-collector/src/platform/node/sendSpans.ts new file mode 100644 index 0000000000..aaf23d2820 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/platform/node/sendSpans.ts @@ -0,0 +1,107 @@ +/*! + * Copyright 2019, 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 http from 'http'; +import * as https from 'https'; + +import { IncomingMessage } from 'http'; +import * as core from '@opentelemetry/core'; +import { CollectorExporter } from '../../CollectorExporter'; + +import * as collectorTypes from '../../types'; + +import * as url from 'url'; +import { VERSION } from '../../version'; + +/** + * function that is called once when {@link ExporterCollector} is initialised + * in node version this is not used + * @param shutdownF shutdown method of {@link ExporterCollector} + */ +export function onInit(shutdownF: Function) {} + +/** + * function to be called once when {@link ExporterCollector} is shutdown + * in node version this is not used + * @param shutdownF - shutdown method of {@link ExporterCollector} + */ +export function onShutdown(shutdownF: Function) {} + +/** + * function to send spans to the [opentelemetry collector]{@link https://github.com/open-telemetry/opentelemetry-collector} + * using the standard http/https node module + * @param spans + * @param onSuccess + * @param onError + * @param collectorExporter + */ +export function sendSpans( + spans: collectorTypes.Span[], + onSuccess: () => void, + onError: (status?: number) => void, + collectorExporter: CollectorExporter +) { + const exportTraceServiceRequest: collectorTypes.ExportTraceServiceRequest = { + 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: '', not implemented + spans, + }; + 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(); +} diff --git a/packages/opentelemetry-exporter-collector/src/transform.ts b/packages/opentelemetry-exporter-collector/src/transform.ts new file mode 100644 index 0000000000..aaf024b025 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/transform.ts @@ -0,0 +1,218 @@ +/*! + * Copyright 2019, 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 { hexToBase64, hrTimeToTimeStamp } from '@opentelemetry/core'; +import { ReadableSpan } from '@opentelemetry/tracing'; +import { Attributes, Link, TimedEvent, TraceState } from '@opentelemetry/types'; +import * as collectorTypes from './types'; + +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 }; +} + +/** + * convert attributes + * @param attributes + */ +export function toCollectorAttributes( + attributes: Attributes +): collectorTypes.Attributes { + const attributeMap: collectorTypes.AttributeMap = {}; + Object.keys(attributes || {}).forEach(key => { + attributeMap[key] = toCollectorEventValue(attributes[key]); + }); + + return { + droppedAttributesCount: 0, + attributeMap, + }; +} + +/** + * convert event value + * @param value event value + */ +export function toCollectorEventValue( + value: unknown +): collectorTypes.AttributeValue { + const attributeValue: collectorTypes.AttributeValue = {}; + + if (typeof value === 'string') { + attributeValue.stringValue = toCollectorTruncatableString(value); + } else if (typeof value === 'boolean') { + attributeValue.boolValue = value; + } else if (typeof value === 'number') { + // all numbers will be treated as double + attributeValue.doubleValue = value; + } + + return attributeValue; +} + +/** + * convert 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, + }; +} + +/** + * 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.spanContext.spanId; + const linkTraceId = link.spanContext.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; +} + +/** + * converts span 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.spanContext.traceId), + spanId: hexToBase64(link.spanContext.spanId), + type: toCollectorLinkType(span, link), + }; + + if (link.attributes) { + collectorLink.attributes = toCollectorAttributes(link.attributes); + } + + return collectorLink; + }); + + return { + link: collectorLinks, + droppedLinksCount: 0, + }; +} + +/** + * @param span + */ +export function toCollectorSpan(span: ReadableSpan): collectorTypes.Span { + return { + traceId: hexToBase64(span.spanContext.traceId), + spanId: hexToBase64(span.spanContext.spanId), + parentSpanId: span.parentSpanId + ? hexToBase64(span.parentSpanId) + : undefined, + tracestate: toCollectorTraceState(span.spanContext.traceState), + name: toCollectorTruncatableString(span.name), + kind: span.kind, + startTime: hrTimeToTimeStamp(span.startTime), + endTime: hrTimeToTimeStamp(span.endTime), + attributes: toCollectorAttributes(span.attributes), + // stackTrace: // not implemented + timeEvents: toCollectorEvents(span.events), + status: span.status, + sameProcessAsParentSpan: !!span.parentSpanId, + links: toCollectorLinks(span), + // childSpanCount: // not implemented + }; +} + +/** + * @param traceState + */ +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; +} diff --git a/packages/opentelemetry-exporter-collector/src/types.ts b/packages/opentelemetry-exporter-collector/src/types.ts new file mode 100644 index 0000000000..1e5cb13b3d --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/types.ts @@ -0,0 +1,520 @@ +/*! + * Copyright 2019, 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 { SpanKind, Status } from '@opentelemetry/types'; + +// 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; +} diff --git a/packages/opentelemetry-exporter-collector/src/version.ts b/packages/opentelemetry-exporter-collector/src/version.ts new file mode 100644 index 0000000000..5e6eb85dbe --- /dev/null +++ b/packages/opentelemetry-exporter-collector/src/version.ts @@ -0,0 +1,18 @@ +/*! + * Copyright 2019, 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. + */ + +// this is autogenerated file, see scripts/version-update.js +export const VERSION = '0.2.0'; diff --git a/packages/opentelemetry-exporter-collector/test/browser/CollectorExporter.test.ts b/packages/opentelemetry-exporter-collector/test/browser/CollectorExporter.test.ts new file mode 100644 index 0000000000..0a11981256 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/test/browser/CollectorExporter.test.ts @@ -0,0 +1,203 @@ +/*! + * Copyright 2019, 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 { + CollectorExporter, + CollectorExporterConfig, +} from '../../src/CollectorExporter'; +import * as collectorTypes from '../../src/types'; + +import { + ensureExportTraceServiceRequestIsSet, + ensureSpanIsCorrect, + mockedReadableSpan, +} from '../helper'; +const sendBeacon = navigator.sendBeacon; + +describe('CollectorExporter - web', () => { + let collectorExporter: CollectorExporter; + let collectorExporterConfig: CollectorExporterConfig; + let spyOpen: any; + let spySend: any; + let spyBeacon: any; + let spans: ReadableSpan[]; + + beforeEach(() => { + spyOpen = sinon.stub(XMLHttpRequest.prototype, 'open'); + spySend = sinon.stub(XMLHttpRequest.prototype, 'send'); + spyBeacon = sinon.stub(navigator, 'sendBeacon'); + collectorExporterConfig = { + hostName: 'foo', + logger: new NoopLogger(), + serviceName: 'bar', + attributes: {}, + url: 'http://foo.bar.com', + }; + collectorExporter = new CollectorExporter(collectorExporterConfig); + spans = []; + spans.push(Object.assign({}, mockedReadableSpan)); + }); + + afterEach(() => { + navigator.sendBeacon = sendBeacon; + spyOpen.restore(); + spySend.restore(); + spyBeacon.restore(); + }); + + describe('export', () => { + describe('when "sendBeacon" is available', () => { + it('should successfully send the spans using sendBeacon', done => { + collectorExporter.export(spans, function() {}); + + setTimeout(() => { + const args = spyBeacon.args[0]; + const url = args[0]; + const body = args[1]; + const json = JSON.parse( + body + ) as collectorTypes.ExportTraceServiceRequest; + const span1 = json.spans && json.spans[0]; + + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + if (span1) { + ensureSpanIsCorrect(span1); + } + assert.strictEqual(url, 'http://foo.bar.com'); + assert.strictEqual(spyBeacon.callCount, 1); + + assert.strictEqual(spyOpen.callCount, 0); + + ensureExportTraceServiceRequestIsSet(json, 10); + + done(); + }); + }); + + it('should log the successful message', done => { + const spyLoggerDebug = sinon.stub(collectorExporter.logger, 'debug'); + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + spyBeacon.restore(); + spyBeacon = sinon.stub(window.navigator, 'sendBeacon').returns(true); + + collectorExporter.export(spans, function() {}); + + setTimeout(() => { + const response: any = spyLoggerDebug.args[1][0]; + assert.strictEqual(response, 'sendBeacon - can send'); + assert.strictEqual(spyLoggerError.args.length, 0); + + done(); + }); + }); + + it('should log the error message', done => { + const spyLoggerDebug = sinon.stub(collectorExporter.logger, 'debug'); + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + spyBeacon.restore(); + spyBeacon = sinon.stub(window.navigator, 'sendBeacon').returns(false); + + collectorExporter.export(spans, function() {}); + + setTimeout(() => { + const response: any = spyLoggerError.args[0][0]; + assert.strictEqual(response, 'sendBeacon - cannot send'); + assert.strictEqual(spyLoggerDebug.args.length, 1); + + done(); + }); + }); + }); + + describe('when "sendBeacon" is NOT available', () => { + let server: any; + beforeEach(() => { + // @ts-ignore + window.navigator.sendBeacon = false; + server = sinon.fakeServer.create(); + }); + afterEach(() => { + server.restore(); + }); + + it('should successfully send the spans using XMLHttpRequest', done => { + collectorExporter.export(spans, function() {}); + + setTimeout(() => { + const request = server.requests[0]; + assert.strictEqual(request.method, 'POST'); + assert.strictEqual(request.url, 'http://foo.bar.com'); + + const body = request.requestBody; + const json = JSON.parse( + body + ) as collectorTypes.ExportTraceServiceRequest; + const span1 = json.spans && json.spans[0]; + + assert.ok(typeof span1 !== 'undefined', "span doesn't exist"); + if (span1) { + ensureSpanIsCorrect(span1); + } + assert.strictEqual(spyBeacon.callCount, 0); + + ensureExportTraceServiceRequestIsSet(json, 10); + + done(); + }); + }); + + it('should log the successful message', done => { + const spyLoggerDebug = sinon.stub(collectorExporter.logger, 'debug'); + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + + collectorExporter.export(spans, function() {}); + + setTimeout(() => { + const request = server.requests[0]; + request.respond(200); + + const response: any = spyLoggerDebug.args[1][0]; + assert.strictEqual(response, 'xhr success'); + assert.strictEqual(spyLoggerError.args.length, 0); + + assert.strictEqual(spyBeacon.callCount, 0); + done(); + }); + }); + + it('should log the error message', done => { + const spyLoggerError = sinon.stub(collectorExporter.logger, 'error'); + + collectorExporter.export(spans, function() {}); + + setTimeout(() => { + const request = server.requests[0]; + request.respond(400); + + const response: any = spyLoggerError.args[0][0]; + assert.strictEqual(response, 'xhr error'); + + assert.strictEqual(spyBeacon.callCount, 0); + done(); + }); + }); + }); + }); +}); diff --git a/packages/opentelemetry-exporter-collector/test/browser/index-webpack.ts b/packages/opentelemetry-exporter-collector/test/browser/index-webpack.ts new file mode 100644 index 0000000000..9fdb7117a2 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/test/browser/index-webpack.ts @@ -0,0 +1,26 @@ +/*! + * Copyright 2019, 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. + */ + +// This file is the webpack entry point for the browser Karma tests. It requires +// all modules ending in "test" from the current folder and all its subfolders. +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-collector/test/common/CollectorExporter.test.ts b/packages/opentelemetry-exporter-collector/test/common/CollectorExporter.test.ts new file mode 100644 index 0000000000..12b7985f1d --- /dev/null +++ b/packages/opentelemetry-exporter-collector/test/common/CollectorExporter.test.ts @@ -0,0 +1,174 @@ +/*! + * Copyright 2019, 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 { ExportResult } from '@opentelemetry/base'; +import { NoopLogger } from '@opentelemetry/core'; +import { ReadableSpan } from '@opentelemetry/tracing'; +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import { + CollectorExporter, + CollectorExporterConfig, +} from '../../src/CollectorExporter'; +import * as collectorTypes from '../../src/types'; +import * as platform from '../../src/platform/index'; + +import { ensureSpanIsCorrect, mockedReadableSpan } from '../helper'; + +describe('CollectorExporter - common', () => { + let collectorExporter: CollectorExporter; + let collectorExporterConfig: CollectorExporterConfig; + + describe('constructor', () => { + let onInitSpy: any; + beforeEach(() => { + onInitSpy = sinon.stub(platform, 'onInit'); + collectorExporterConfig = { + hostName: 'foo', + logger: new NoopLogger(), + serviceName: 'bar', + attributes: {}, + url: 'http://foo.bar.com', + }; + collectorExporter = new CollectorExporter(collectorExporterConfig); + }); + afterEach(() => { + onInitSpy.restore(); + }); + + it('should create an instance', () => { + assert.ok(typeof collectorExporter !== 'undefined'); + }); + + it('should call onInit', () => { + assert.strictEqual(onInitSpy.callCount, 1); + assert.ok(onInitSpy.args[0][0] === collectorExporter.shutdown); + }); + + describe('when config contains certain params', () => { + it('should set hostName', () => { + assert.strictEqual(collectorExporter.hostName, 'foo'); + }); + + it('should set serviceName', () => { + assert.strictEqual(collectorExporter.serviceName, 'bar'); + }); + + it('should set url', () => { + assert.strictEqual(collectorExporter.url, 'http://foo.bar.com'); + }); + + it('should set logger', () => { + assert.ok(collectorExporter.logger === collectorExporterConfig.logger); + }); + }); + + describe('when config is missing certain params', () => { + beforeEach(() => { + collectorExporter = new CollectorExporter(); + }); + + it('should set default serviceName', () => { + assert.strictEqual(collectorExporter.serviceName, 'collector-exporter'); + }); + + it('should set default logger', () => { + assert.ok(collectorExporter.logger instanceof NoopLogger); + }); + }); + }); + + describe('export', () => { + let spySend: any; + beforeEach(() => { + spySend = sinon.stub(platform, 'sendSpans'); + collectorExporter = new CollectorExporter(collectorExporterConfig); + }); + afterEach(() => { + spySend.restore(); + }); + + it('should export spans as collectorTypes.Spans', done => { + const spans: ReadableSpan[] = []; + spans.push(Object.assign({}, mockedReadableSpan)); + + collectorExporter.export(spans, function() {}); + setTimeout(() => { + const span1 = spySend.args[0][0][0] as collectorTypes.Span; + ensureSpanIsCorrect(span1); + done(); + }); + assert.strictEqual(spySend.callCount, 1); + }); + + describe('when exporter is shutdown', () => { + it('should not export anything but return callback with code "FailedNotRetryable"', () => { + const spans: ReadableSpan[] = []; + spans.push(Object.assign({}, mockedReadableSpan)); + collectorExporter.shutdown(); + spySend.resetHistory(); + + const callbackSpy = sinon.spy(); + collectorExporter.export(spans, callbackSpy); + const returnCode = callbackSpy.args[0][0]; + + assert.strictEqual( + returnCode, + ExportResult.FAILED_NOT_RETRYABLE, + 'return value is wrong' + ); + assert.strictEqual(spySend.callCount, 0, 'should not call send'); + }); + }); + }); + + describe('shutdown', () => { + let onShutdownSpy: any; + beforeEach(() => { + onShutdownSpy = sinon.stub(platform, 'onShutdown'); + collectorExporterConfig = { + hostName: 'foo', + logger: new NoopLogger(), + serviceName: 'bar', + attributes: {}, + url: 'http://foo.bar.com', + }; + collectorExporter = new CollectorExporter(collectorExporterConfig); + }); + afterEach(() => { + onShutdownSpy.restore(); + }); + + it('should export spans once only', done => { + collectorExporter.shutdown(); + collectorExporter.shutdown(); + collectorExporter.shutdown(); + + setTimeout(() => { + assert.strictEqual(onShutdownSpy.callCount, 1); + done(); + }); + }); + + it('should call onShutdown', done => { + collectorExporter.shutdown(); + setTimeout(() => { + assert.ok(onShutdownSpy.args[0][0] === collectorExporter.shutdown); + done(); + }); + }); + }); +}); diff --git a/packages/opentelemetry-exporter-collector/test/common/transform.test.ts b/packages/opentelemetry-exporter-collector/test/common/transform.test.ts new file mode 100644 index 0000000000..d1227f04a8 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/test/common/transform.test.ts @@ -0,0 +1,152 @@ +/*! + * Copyright 2019, 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 { Attributes, TimedEvent } from '@opentelemetry/types'; +import * as assert from 'assert'; +import * as transform from '../../src/transform'; +import { ensureSpanIsCorrect, mockedReadableSpan } from '../helper'; + +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, + }); + }); + + it('should convert attribute integer', () => { + const attributes: Attributes = { + foo: 13, + }; + assert.deepStrictEqual(transform.toCollectorAttributes(attributes), { + attributeMap: { + foo: { + doubleValue: 13, + }, + }, + droppedAttributesCount: 0, + }); + }); + + it('should convert attribute boolean', () => { + const attributes: Attributes = { + foo: true, + }; + assert.deepStrictEqual(transform.toCollectorAttributes(attributes), { + attributeMap: { + foo: { + boolValue: true, + }, + }, + droppedAttributesCount: 0, + }); + }); + + it('should convert attribute double', () => { + const attributes: Attributes = { + foo: 1.34, + }; + assert.deepStrictEqual(transform.toCollectorAttributes(attributes), { + attributeMap: { + foo: { + doubleValue: 1.34, + }, + }, + droppedAttributesCount: 0, + }); + }); + }); + + describe('toCollectorEvents', () => { + it('should convert events to otc events', () => { + const events: TimedEvent[] = [ + { name: 'foo', time: [123, 123], attributes: { a: 'b' } }, + { + name: 'foo2', + time: [321, 321], + 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, + }); + }); + }); + + describe('toCollectorSpan', () => { + it('should convert span', () => { + ensureSpanIsCorrect(transform.toCollectorSpan(mockedReadableSpan)); + }); + }); +}); diff --git a/packages/opentelemetry-exporter-collector/test/helper.ts b/packages/opentelemetry-exporter-collector/test/helper.ts new file mode 100644 index 0000000000..46071b7e7f --- /dev/null +++ b/packages/opentelemetry-exporter-collector/test/helper.ts @@ -0,0 +1,195 @@ +/*! + * Copyright 2019, 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 assert from 'assert'; +import * as transform from '../src/transform'; +import * as collectorTypes from '../src/types'; +import { VERSION } from '../src/version'; + +export const mockedReadableSpan: ReadableSpan = { + name: 'documentFetch', + kind: 0, + spanContext: { + traceId: '1f1008dc8e270e85c40a0d7c3939b278', + spanId: '5e107261f64fa53e', + traceFlags: 1, + }, + parentSpanId: '78a8915098864388', + startTime: [1574120165, 429803070], + endTime: [1574120165, 438688070], + status: { code: 0 }, + attributes: { component: 'document-load' }, + links: [ + { + spanContext: { + traceId: '1f1008dc8e270e85c40a0d7c3939b278', + spanId: '78a8915098864388', + traceFlags: 1, + }, + attributes: { component: 'document-load' }, + }, + ], + events: [ + { name: 'fetchStart', time: [1574120165, 429803070] }, + { + name: 'domainLookupStart', + time: [1574120165, 429803070], + }, + { name: 'domainLookupEnd', time: [1574120165, 429803070] }, + { + name: 'connectStart', + time: [1574120165, 429803070], + }, + { name: 'connectEnd', time: [1574120165, 429803070] }, + { + name: 'requestStart', + time: [1574120165, 435513070], + }, + { name: 'responseStart', time: [1574120165, 436923070] }, + { + name: 'responseEnd', + time: [1574120165, 438688070], + }, + ], + duration: [0, 8885000], +}; + +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 }, + }, + }, + }, + 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 }, + }, + }, + ], + 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 }, + }, + }, + }, + }, + ], + }, + }); +} + +export function ensureExportTraceServiceRequestIsSet( + json: collectorTypes.ExportTraceServiceRequest, + languageInfo: collectorTypes.LibraryInfoLanguage +) { + const libraryInfo = json.node && json.node.libraryInfo; + const serviceInfo = json.node && json.node.serviceInfo; + const identifier = json.node && json.node.identifier; + + const language = libraryInfo && libraryInfo.language; + assert.strictEqual(language, languageInfo, 'language is missing'); + + const exporterVersion = libraryInfo && libraryInfo.exporterVersion; + assert.strictEqual(exporterVersion, VERSION, 'version is missing'); + + const coreVersion = libraryInfo && libraryInfo.coreLibraryVersion; + assert.strictEqual(coreVersion, core.VERSION, 'core version is missing'); + + const name = serviceInfo && serviceInfo.name; + assert.strictEqual(name, 'bar', 'name is missing'); + + const hostName = identifier && identifier.hostName; + assert.strictEqual(hostName, 'foo', 'hostName is missing'); +} diff --git a/packages/opentelemetry-exporter-collector/test/node/CollectorExporter.test.ts b/packages/opentelemetry-exporter-collector/test/node/CollectorExporter.test.ts new file mode 100644 index 0000000000..f057ce471d --- /dev/null +++ b/packages/opentelemetry-exporter-collector/test/node/CollectorExporter.test.ts @@ -0,0 +1,149 @@ +/*! + * Copyright 2019, 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 { + CollectorExporter, + CollectorExporterConfig, +} from '../../src/CollectorExporter'; +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', () => { + 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, '/'); + 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); + + 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(); + }); + }); + }); + }); +}); diff --git a/packages/opentelemetry-exporter-collector/tsconfig-release.json b/packages/opentelemetry-exporter-collector/tsconfig-release.json new file mode 100644 index 0000000000..ffc0f77968 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/tsconfig-release.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": [] + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/opentelemetry-exporter-collector/tsconfig.json b/packages/opentelemetry-exporter-collector/tsconfig.json new file mode 100644 index 0000000000..a2042cd68b --- /dev/null +++ b/packages/opentelemetry-exporter-collector/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/packages/opentelemetry-exporter-collector/tslint.json b/packages/opentelemetry-exporter-collector/tslint.json new file mode 100644 index 0000000000..0710b135d0 --- /dev/null +++ b/packages/opentelemetry-exporter-collector/tslint.json @@ -0,0 +1,4 @@ +{ + "rulesDirectory": ["node_modules/tslint-microsoft-contrib"], + "extends": ["../../tslint.base.js", "./node_modules/tslint-consistent-codestyle"] +}