From 4f48357b7d1dea246a67ef1a0e071fc64aa10e26 Mon Sep 17 00:00:00 2001 From: Bartlomiej Obecny Date: Fri, 8 Nov 2019 22:08:21 +0100 Subject: [PATCH 01/17] feat(traceparent): setting parent span from server (#477) * feat(traceparent): setting parent span from server * chore: exposing the parse functionality * chore: refactored to use existing functionality * chore: adding jsdoc to exported function * chore: updating readme with example for traceparent * chore: moving the traceparent to meta instead of window * chore: updating the jsdoc * chore: updating the copy as suggested --- examples/tracer-web/index.html | 11 +++++ .../context/propagation/HttpTraceContext.ts | 14 +++++- .../README.md | 29 +++++++++++ .../src/documentLoad.ts | 16 +++++- .../test/documentLoad.test.ts | 49 ++++++++++++++++++- 5 files changed, 114 insertions(+), 5 deletions(-) diff --git a/examples/tracer-web/index.html b/examples/tracer-web/index.html index 08bab8e1b0c..4e9afa7bd64 100644 --- a/examples/tracer-web/index.html +++ b/examples/tracer-web/index.html @@ -6,6 +6,17 @@ Web Tracer Example + + + diff --git a/packages/opentelemetry-core/src/context/propagation/HttpTraceContext.ts b/packages/opentelemetry-core/src/context/propagation/HttpTraceContext.ts index f964f2273ef..b003572f2bc 100644 --- a/packages/opentelemetry-core/src/context/propagation/HttpTraceContext.ts +++ b/packages/opentelemetry-core/src/context/propagation/HttpTraceContext.ts @@ -25,7 +25,17 @@ const VALID_SPANID_REGEX = /^[0-9a-f]{16}$/i; const INVALID_ID_REGEX = /^0+$/i; const VERSION = '00'; -function parse(traceParent: string): SpanContext | null { +/** + * Parses information from the [traceparent] span tag and converts it into {@link SpanContext} + * @param traceParent - A meta property that comes from server. + * It should be dynamically generated server side to have the server's request trace Id, + * a parent span Id that was set on the server's request span, + * and the trace flags to indicate the server's sampling decision + * (01 = sampled, 00 = not sampled). + * for example: '{version}-{traceId}-{spanId}-{sampleDecision}' + * For more information see {@link https://www.w3.org/TR/trace-context/} + */ +export function parseTraceParent(traceParent: string): SpanContext | null { const match = traceParent.match(VALID_TRACE_PARENT_REGEX); if (!match) return null; const parts = traceParent.split('-'); @@ -87,7 +97,7 @@ export class HttpTraceContext implements HttpTextFormat { const traceParent = Array.isArray(traceParentHeader) ? traceParentHeader[0] : traceParentHeader; - const spanContext = parse(traceParent); + const spanContext = parseTraceParent(traceParent); if (!spanContext) return null; spanContext.isRemote = true; diff --git a/packages/opentelemetry-plugin-document-load/README.md b/packages/opentelemetry-plugin-document-load/README.md index cfc8346618f..a3a8d30533a 100644 --- a/packages/opentelemetry-plugin-document-load/README.md +++ b/packages/opentelemetry-plugin-document-load/README.md @@ -29,6 +29,34 @@ const webTracer = new WebTracer({ webTracer.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); ``` +## Optional: Send a trace parent from your server +This plugin supports connecting the server side spans for the initial HTML load with the client side span for the load from the browser's timing API. This works by having the server send its parent trace context (trace ID, span ID and trace sampling decision) to the client. + +Because the browser does not send a trace context header for the initial page navigation, the server needs to fake a trace context header in a middleware and then send that trace context header back to the client as a meta tag *traceparent* . The *traceparent* meta tag should be in the [trace context W3C draft format][trace-context-url] . For example: + +```html + ... + + + + + + ... + + +``` + ## Useful links - For more information on OpenTelemetry, visit: - For more about OpenTelemetry JavaScript: @@ -48,3 +76,4 @@ Apache 2.0 - See [LICENSE][license-url] for more information. [devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-plugin-document-load&type=dev [npm-url]: https://www.npmjs.com/package/@opentelemetry/plugin-document-load [npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fplugin-document-load.svg +[trace-context-url]: https://www.w3.org/TR/trace-context diff --git a/packages/opentelemetry-plugin-document-load/src/documentLoad.ts b/packages/opentelemetry-plugin-document-load/src/documentLoad.ts index 9770decd463..0ffea50d310 100644 --- a/packages/opentelemetry-plugin-document-load/src/documentLoad.ts +++ b/packages/opentelemetry-plugin-document-load/src/documentLoad.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -import { BasePlugin, otperformance } from '@opentelemetry/core'; +import { + BasePlugin, + otperformance, + parseTraceParent, + TRACE_PARENT_HEADER, +} from '@opentelemetry/core'; import { PluginConfig, Span, SpanOptions } from '@opentelemetry/types'; import { AttributeNames } from './enums/AttributeNames'; import { PerformanceTimingNames as PTN } from './enums/PerformanceTimingNames'; @@ -106,12 +111,19 @@ export class DocumentLoad extends BasePlugin { * Collects information about performance and creates appropriate spans */ private _collectPerformance() { + const metaElement = [...document.getElementsByTagName('meta')].find( + e => e.getAttribute('name') === TRACE_PARENT_HEADER + ); + const serverContext = + parseTraceParent((metaElement && metaElement.content) || '') || undefined; + const entries = this._getEntries(); const rootSpan = this._startSpan( AttributeNames.DOCUMENT_LOAD, PTN.FETCH_START, - entries + entries, + { parent: serverContext } ); if (!rootSpan) { return; diff --git a/packages/opentelemetry-plugin-document-load/test/documentLoad.test.ts b/packages/opentelemetry-plugin-document-load/test/documentLoad.test.ts index 3c317286c45..5446adc613c 100644 --- a/packages/opentelemetry-plugin-document-load/test/documentLoad.test.ts +++ b/packages/opentelemetry-plugin-document-load/test/documentLoad.test.ts @@ -21,7 +21,7 @@ import * as assert from 'assert'; import * as sinon from 'sinon'; -import { ConsoleLogger } from '@opentelemetry/core'; +import { ConsoleLogger, TRACE_PARENT_HEADER } from '@opentelemetry/core'; import { BasicTracer, ReadableSpan, @@ -306,6 +306,53 @@ describe('DocumentLoad Plugin', () => { done(); }); }); + + describe('AND window has information about server root span', () => { + let spyGetElementsByTagName: any; + beforeEach(() => { + const element = { + content: '00-ab42124a3c573678d4d8b21ba52df3bf-d21f7bc17caa5aba-01', + getAttribute: (value: string) => { + if (value === 'name') { + return TRACE_PARENT_HEADER; + } + return undefined; + }, + }; + + spyGetElementsByTagName = sinon.stub( + window.document, + 'getElementsByTagName' + ); + spyGetElementsByTagName.withArgs('meta').returns([element]); + }); + afterEach(() => { + spyGetElementsByTagName.restore(); + }); + + it('should create a root span with server context traceId', done => { + const spyOnEnd = sinon.spy(dummyExporter, 'export'); + plugin.enable(moduleExports, tracer, logger, config); + setTimeout(() => { + const rootSpan = spyOnEnd.args[0][0][0] as ReadableSpan; + const fetchSpan = spyOnEnd.args[1][0][0] as ReadableSpan; + assert.strictEqual(rootSpan.name, 'documentFetch'); + assert.strictEqual(fetchSpan.name, 'documentLoad'); + + assert.strictEqual( + rootSpan.spanContext.traceId, + 'ab42124a3c573678d4d8b21ba52df3bf' + ); + assert.strictEqual( + fetchSpan.spanContext.traceId, + 'ab42124a3c573678d4d8b21ba52df3bf' + ); + + assert.strictEqual(spyOnEnd.callCount, 2); + done(); + }, 1); + }); + }); }); describe('when resource entries are available', () => { From f5801d2b5f7415540a43ffe3590401159a74da53 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Fri, 8 Nov 2019 13:58:02 -0800 Subject: [PATCH 02/17] Propose to add @obecny as the CODEOWNERS. (#505) * chore: add @obecny in CODEOWNERS * chore: update approvers list --- .github/CODEOWNERS | 2 +- README.md | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 0057c0d7bb0..a55bb2918c3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -12,4 +12,4 @@ # https://help.github.com/en/articles/about-code-owners # -* @danielkhan @mayurkale22 @rochdev @bg451 @OlivierAlbertini @vmarchaud @markwolff +* @danielkhan @mayurkale22 @rochdev @bg451 @OlivierAlbertini @vmarchaud @markwolff @obecny diff --git a/README.md b/README.md index fb0d120126e..8e18c085535 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ Approvers ([@open-telemetry/js-approvers](https://github.com/orgs/open-telemetry - [Olivier Albertini](https://github.com/OlivierAlbertini), VilledeMontreal - [Valentin Marchaud](https://github.com/vmarchaud), Open Source Contributor - [Mark Wolff](https://github.com/markwolff), Microsoft +- [Bartlomiej Obecny](https://github.com/obecny), LightStep *Find more about the approver role in [community repository](https://github.com/open-telemetry/community/blob/master/community-membership.md#approver).* From 16674a77958444414702626ed341285cfd5c5aae Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Sat, 9 Nov 2019 14:23:25 -0800 Subject: [PATCH 03/17] chore: remove test script from types package (#511) --- packages/opentelemetry-types/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opentelemetry-types/package.json b/packages/opentelemetry-types/package.json index 12f1bc7a692..58288c09c4b 100644 --- a/packages/opentelemetry-types/package.json +++ b/packages/opentelemetry-types/package.json @@ -11,7 +11,6 @@ "precompile": "tsc --version", "compile": "tsc -p .", "fix": "gts fix", - "test": "npm run compile && npm run check", "docs-test": "linkinator docs/out", "docs": "typedoc --tsconfig tsconfig.json", "prepare": "npm run compile" From f180a77dea4efd10481df5e477144deee3f3192b Mon Sep 17 00:00:00 2001 From: Daniel Dyla Date: Mon, 11 Nov 2019 11:33:48 -0500 Subject: [PATCH 04/17] ci: install minimal lint & doc deps (#508) * ci: install minimal lint & doc deps * fix: lint --- .circleci/config.yml | 7 +++++-- .../src/documentLoad.ts | 2 +- packages/opentelemetry-plugin-grpc/src/grpc.ts | 9 ++++++--- packages/opentelemetry-plugin-http/src/http.ts | 2 +- packages/opentelemetry-plugin-http/src/types.ts | 2 +- packages/opentelemetry-plugin-http/src/utils.ts | 4 +++- .../opentelemetry-plugin-pg/src/pg.ts | 7 ++++--- 7 files changed, 21 insertions(+), 12 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 3d72787e934..4134298bfa6 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -85,8 +85,11 @@ jobs: steps: - checkout - run: - name: Install modules and dependencies. - command: yarn install + name: Install minimal doc and lint modules globally + command: yarn global add lerna typedoc linkinator typescript gts tslint-consistent-codestyle tslint-microsoft-contrib + - run: + name: Symlink global modules into all lerna packages + command: lerna exec 'ln -s $(yarn global dir)/node_modules node_modules' - run: name: Check code style and linting command: yarn run check diff --git a/packages/opentelemetry-plugin-document-load/src/documentLoad.ts b/packages/opentelemetry-plugin-document-load/src/documentLoad.ts index 0ffea50d310..520c2b24b0b 100644 --- a/packages/opentelemetry-plugin-document-load/src/documentLoad.ts +++ b/packages/opentelemetry-plugin-document-load/src/documentLoad.ts @@ -198,7 +198,7 @@ export class DocumentLoad extends BasePlugin { }); } else { // // fallback to previous version - const perf: (typeof otperformance) & PerformanceLegacy = otperformance; + const perf: typeof otperformance & PerformanceLegacy = otperformance; const performanceTiming = perf.timing; if (performanceTiming) { const keys = Object.values(PTN); diff --git a/packages/opentelemetry-plugin-grpc/src/grpc.ts b/packages/opentelemetry-plugin-grpc/src/grpc.ts index 531bd80bbf6..9c44c65abd4 100644 --- a/packages/opentelemetry-plugin-grpc/src/grpc.ts +++ b/packages/opentelemetry-plugin-grpc/src/grpc.ts @@ -354,9 +354,12 @@ export class GrpcPlugin extends BasePlugin { parent: currentSpan || undefined, }) .setAttribute(AttributeNames.COMPONENT, GrpcPlugin.component); - return plugin._makeGrpcClientRemoteCall(original, args, this, plugin)( - span - ); + return plugin._makeGrpcClientRemoteCall( + original, + args, + this, + plugin + )(span); }; }; } diff --git a/packages/opentelemetry-plugin-http/src/http.ts b/packages/opentelemetry-plugin-http/src/http.ts index 76278b5760c..22c34c62b64 100644 --- a/packages/opentelemetry-plugin-http/src/http.ts +++ b/packages/opentelemetry-plugin-http/src/http.ts @@ -485,7 +485,7 @@ export class HttpPlugin extends BasePlugin { span: Span, execute: T, rethrow: K - ): K extends true ? ReturnType : (ReturnType | void); + ): K extends true ? ReturnType : ReturnType | void; private _safeExecute ReturnType>( span: Span, execute: T, diff --git a/packages/opentelemetry-plugin-http/src/types.ts b/packages/opentelemetry-plugin-http/src/types.ts index 05fe0338630..e55c3583483 100644 --- a/packages/opentelemetry-plugin-http/src/types.ts +++ b/packages/opentelemetry-plugin-http/src/types.ts @@ -39,7 +39,7 @@ export type RequestSignature = [http.RequestOptions, HttpCallbackOptional] & export type HttpRequestArgs = Array; export type ParsedRequestOptions = - | http.RequestOptions & Partial + | (http.RequestOptions & Partial) | http.RequestOptions; export type Http = typeof http; /* tslint:disable-next-line:no-any */ diff --git a/packages/opentelemetry-plugin-http/src/utils.ts b/packages/opentelemetry-plugin-http/src/utils.ts index b1a27b97fd8..fbada00b1fa 100644 --- a/packages/opentelemetry-plugin-http/src/utils.ts +++ b/packages/opentelemetry-plugin-http/src/utils.ts @@ -46,7 +46,9 @@ export const getAbsoluteUrl = ( // it should be displayed if it's not 80 and 443 (default ports) if ( (host as string).indexOf(':') === -1 && - (port && port !== '80' && port !== '443') + port && + port !== '80' && + port !== '443' ) { host += `:${port}`; } diff --git a/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/src/pg.ts b/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/src/pg.ts index e41ceee08a0..a6841ae33a8 100644 --- a/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/src/pg.ts +++ b/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/src/pg.ts @@ -97,9 +97,10 @@ export class PostgresPlugin extends BasePlugin { const parentSpan = plugin._tracer.getCurrentSpan(); if (typeof args[args.length - 1] === 'function') { // Patch ParameterQuery callback - args[args.length - 1] = utils.patchCallback(span, args[ - args.length - 1 - ] as PostgresCallback); + args[args.length - 1] = utils.patchCallback( + span, + args[args.length - 1] as PostgresCallback + ); // If a parent span exists, bind the callback if (parentSpan) { args[args.length - 1] = plugin._tracer.bind( From 3d01be67fd7279ec451123ec421b015a172a9db3 Mon Sep 17 00:00:00 2001 From: Bartlomiej Obecny Date: Mon, 11 Nov 2019 17:53:13 +0100 Subject: [PATCH 05/17] feat(scope-zone): new scope manager to support async operations in web (#461) * feat(scope-zone): new scope manager to support async operations in web * chore: removing not needed dependency * chore: updating readme * chore: clean up * chore: refactored the bind method * chore: updated example for web tracer * chore: updated readme * chore: updating jsdoc * fix: fixing flaky test for BatchSpanProcessor with timeout * chore: missing link * chore: updating karma files * chore: adding docs and tests for utils * chore: refactoring zone scope manager * chore: adding test for web and zone scope manager for parallel actions * chore: updating example for web tracer with zone scope manager * chore: updating example for web tracer with zone scope manager * chore: adding test for parallel run --- README.md | 4 + examples/tracer-web/index.html | 3 + examples/tracer-web/index.js | 74 ++++- examples/tracer-web/package.json | 1 + examples/tracer-web/webpack.config.js | 10 - .../opentelemetry-scope-zone-peer-dep/LICENSE | 201 ++++++++++++ .../README.md | 71 +++++ .../karma.conf.js | 24 ++ .../package.json | 80 +++++ .../src/ZoneScopeManager.ts | 258 ++++++++++++++++ .../src/index.ts | 18 ++ .../src/types.ts | 35 +++ .../src/util.ts | 28 ++ .../test/ZoneScopeManager.test.ts | 290 ++++++++++++++++++ .../test/index-webpack.ts | 23 ++ .../test/utils.test.ts | 54 ++++ .../tsconfig.json | 12 + .../tslint.json | 4 + packages/opentelemetry-scope-zone/LICENSE | 201 ++++++++++++ packages/opentelemetry-scope-zone/README.md | 71 +++++ .../opentelemetry-scope-zone/package.json | 73 +++++ .../opentelemetry-scope-zone/src/index.ts | 18 ++ .../opentelemetry-scope-zone/tsconfig.json | 12 + packages/opentelemetry-scope-zone/tslint.json | 4 + packages/opentelemetry-web/README.md | 49 +-- packages/opentelemetry-web/package.json | 3 +- .../opentelemetry-web/test/WebTracer.test.ts | 42 +++ 27 files changed, 1632 insertions(+), 31 deletions(-) create mode 100644 packages/opentelemetry-scope-zone-peer-dep/LICENSE create mode 100644 packages/opentelemetry-scope-zone-peer-dep/README.md create mode 100644 packages/opentelemetry-scope-zone-peer-dep/karma.conf.js create mode 100644 packages/opentelemetry-scope-zone-peer-dep/package.json create mode 100644 packages/opentelemetry-scope-zone-peer-dep/src/ZoneScopeManager.ts create mode 100644 packages/opentelemetry-scope-zone-peer-dep/src/index.ts create mode 100644 packages/opentelemetry-scope-zone-peer-dep/src/types.ts create mode 100644 packages/opentelemetry-scope-zone-peer-dep/src/util.ts create mode 100644 packages/opentelemetry-scope-zone-peer-dep/test/ZoneScopeManager.test.ts create mode 100644 packages/opentelemetry-scope-zone-peer-dep/test/index-webpack.ts create mode 100644 packages/opentelemetry-scope-zone-peer-dep/test/utils.test.ts create mode 100644 packages/opentelemetry-scope-zone-peer-dep/tsconfig.json create mode 100644 packages/opentelemetry-scope-zone-peer-dep/tslint.json create mode 100644 packages/opentelemetry-scope-zone/LICENSE create mode 100644 packages/opentelemetry-scope-zone/README.md create mode 100644 packages/opentelemetry-scope-zone/package.json create mode 100644 packages/opentelemetry-scope-zone/src/index.ts create mode 100644 packages/opentelemetry-scope-zone/tsconfig.json create mode 100644 packages/opentelemetry-scope-zone/tslint.json diff --git a/README.md b/README.md index 8e18c085535..95d5b443796 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,7 @@ OpenTelemetry is vendor-agnostic and can upload data to any backend with various OpenTelemetry can collect tracing data automatically using plugins. Vendors/Users can also create and use their own. Currently, OpenTelemetry supports automatic tracing for: +#### Node Plugins - [@opentelemetry/plugin-http](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-http) - [@opentelemetry/plugin-grpc](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-grpc) - [@opentelemetry/plugin-https](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-https) @@ -112,6 +113,9 @@ OpenTelemetry can collect tracing data automatically using plugins. Vendors/User - [@opentelemetry/plugin-mongodb](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-mongodb) - WIP - [@opentelemetry/plugin-postgres](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-postgres) - WIP +#### Web Plugins +- [@opentelemetry/plugin-document-load](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-document-load) + To request automatic tracing support for a module not on this list, please [file an issue](https://github.com/open-telemetry/opentelemetry-js/issues). Alternatively, you can [write a plugin yourself](https://github.com/open-telemetry/opentelemetry-js/blob/master/doc/plugin-guide.md). ### Shims diff --git a/examples/tracer-web/index.html b/examples/tracer-web/index.html index 4e9afa7bd64..947630827c0 100644 --- a/examples/tracer-web/index.html +++ b/examples/tracer-web/index.html @@ -23,6 +23,9 @@ Example of using Web Tracer with document load plugin and console exporter +
+ + diff --git a/examples/tracer-web/index.js b/examples/tracer-web/index.js index a8e3e34b413..0e5e9956e33 100644 --- a/examples/tracer-web/index.js +++ b/examples/tracer-web/index.js @@ -1,11 +1,83 @@ import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; import { WebTracer } from '@opentelemetry/web'; import { DocumentLoad } from '@opentelemetry/plugin-document-load'; +import { ZoneScopeManager } from '@opentelemetry/scope-zone'; const webTracer = new WebTracer({ plugins: [ new DocumentLoad() ] }); - webTracer.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); + +const webTracerWithZone = new WebTracer({ + scopeManager: new ZoneScopeManager(), + plugins: [ + new DocumentLoad() + ] +}); +webTracerWithZone.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); + +console.log('Current span is window', webTracerWithZone.getCurrentSpan() === window); + +// example of keeping track of scope between async operations +const prepareClickEvent = () => { + const url1 = 'https://raw.githubusercontent.com/open-telemetry/opentelemetry-js/master/package.json'; + const url2 = 'https://raw.githubusercontent.com/open-telemetry/opentelemetry-js/master/packages/opentelemetry-web/package.json'; + + const element = document.getElementById('button1'); + let mainSpan = webTracerWithZone.startSpan('main-span'); + webTracerWithZone.bind(element, mainSpan); + + const onClick = () => { + const span1 = webTracerWithZone.startSpan(`files-series-info-1`, { + parent: webTracerWithZone.getCurrentSpan() + }); + + const span2 = webTracerWithZone.startSpan(`files-series-info-2`, { + parent: webTracerWithZone.getCurrentSpan() + }); + + webTracerWithZone.withSpan(span1, () => { + getData(url1).then((data) => { + console.log('current span is span1', webTracerWithZone.getCurrentSpan() === span1); + console.log('info from package.json', data.description, data.version); + webTracerWithZone.getCurrentSpan().addEvent('fetching-span1-completed'); + span1.end(); + }); + }); + + webTracerWithZone.withSpan(span2, () => { + getData(url2).then((data) => { + setTimeout(() => { + console.log('current span is span2', webTracerWithZone.getCurrentSpan() === span2); + console.log('info from package.json', data.description, data.version); + webTracerWithZone.getCurrentSpan().addEvent('fetching-span2-completed'); + span2.end(); + }, 100); + }); + }); + }; + element.addEventListener('click', onClick); +}; + +const getData = (url) => { + return new Promise(async (resolve, reject) => { + const req = new XMLHttpRequest(); + req.open('GET', url, true); + req.send(); + req.onload = function () { + let json; + try { + json = JSON.parse(req.responseText); + } catch (e) { + reject(e); + } + resolve(json); + }; + }); +}; + +window.addEventListener('load', () => { + prepareClickEvent(); +}); diff --git a/examples/tracer-web/package.json b/examples/tracer-web/package.json index bc38aa6a1de..35c98c82408 100644 --- a/examples/tracer-web/package.json +++ b/examples/tracer-web/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@opentelemetry/plugin-document-load": "^0.2.0", + "@opentelemetry/scope-zone": "^0.2.0", "@opentelemetry/tracing": "^0.2.0", "@opentelemetry/web": "^0.2.0" }, diff --git a/examples/tracer-web/webpack.config.js b/examples/tracer-web/webpack.config.js index 69ad1b74f29..0c4efe02b24 100644 --- a/examples/tracer-web/webpack.config.js +++ b/examples/tracer-web/webpack.config.js @@ -26,14 +26,6 @@ const common = { } ] }, - plugins: [ - new webpack.ProvidePlugin({ - jQuery: 'jquery', - $: 'jquery', - jquery: 'jquery', - 'window.jQuery': 'jquery' - }) - ], resolve: { modules: [ path.resolve(mainPath, 'src'), @@ -52,8 +44,6 @@ module.exports = webpackMerge(common, { }, devServer: { contentBase: path.resolve(__dirname), - // contentBase: path.resolve('.'), - // historyApiFallback: true }, plugins: [ new webpack.DefinePlugin({ diff --git a/packages/opentelemetry-scope-zone-peer-dep/LICENSE b/packages/opentelemetry-scope-zone-peer-dep/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/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-scope-zone-peer-dep/README.md b/packages/opentelemetry-scope-zone-peer-dep/README.md new file mode 100644 index 00000000000..72d83ed591d --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/README.md @@ -0,0 +1,71 @@ +# OpenTelemetry Scope Zone Peer Dependency +[![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 *Zone Scope Manager with a peer dependency for [zone-js]* for Web applications. +If you use Angular you already have the [zone-js] and you should use this package. +If you don't have your own [zone-js] please use [@opentelemetry/scope-zone] + + +## Installation + +```bash +npm install --save @opentelemetry/scope-zone-peer-dep +``` + +## Usage +```js +import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; +import { WebTracer } from '@opentelemetry/web'; +import { ZoneScopeManager } from '@opentelemetry/scope-zone-peer-dep'; + +const webTracerWithZone = new WebTracer({ + scopeManager: new ZoneScopeManager() +}); +webTracerWithZone.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); + +// Example how the ZoneScopeManager keeps the reference to the correct scope during async operations +const span1 = webTracerWithZone.startSpan('foo1'); +webTracerWithZone.withSpan(span1, () => { + console.log('Current span is span1', webTracerWithZone.getCurrentSpan() === span1); + setTimeout(() => { + const span2 = webTracerWithZone.startSpan('foo2'); + console.log('Current span is span1', webTracerWithZone.getCurrentSpan() === span1); + webTracerWithZone.withSpan(span2, () => { + console.log('Current span is span2', webTracerWithZone.getCurrentSpan() === span2); + setTimeout(() => { + console.log('Current span is span2', webTracerWithZone.getCurrentSpan() === span2); + }, 500); + }); + // there is a timeout which still keeps span2 active + console.log('Current span is span2', webTracerWithZone.getCurrentSpan() === span2); + }, 500); + console.log('Current span is span1', webTracerWithZone.getCurrentSpan() === span1); +}); + +``` + +## 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-scope-zone-peer-dep +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-scope-zone-peer-dep +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-scope-zone-peer-dep +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-web&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/scope-zone-peer-dep +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fscope-zone-peer-dep.svg +[zone-js]: https://www.npmjs.com/package/zone.js +[@opentelemetry/scope-zone]: https://www.npmjs.com/package/@opentelemetry/scope-zone diff --git a/packages/opentelemetry-scope-zone-peer-dep/karma.conf.js b/packages/opentelemetry-scope-zone-peer-dep/karma.conf.js new file mode 100644 index 00000000000..7183aab0336 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/karma.conf.js @@ -0,0 +1,24 @@ +/*! + * 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 + })) +}; diff --git a/packages/opentelemetry-scope-zone-peer-dep/package.json b/packages/opentelemetry-scope-zone-peer-dep/package.json new file mode 100644 index 00000000000..0497800f462 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/package.json @@ -0,0 +1,80 @@ +{ + "name": "@opentelemetry/scope-zone-peer-dep", + "version": "0.2.0", + "description": "OpenTelemetry Scope Zone with peer dependency for zone.js", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "check": "gts check", + "clean": "rimraf build/*", + "codecov:browser": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "compile": "tsc -p .", + "fix": "gts fix", + "prepare": "npm run compile", + "tdd": "karma start", + "test:browser": "nyc karma start --single-run", + "watch": "tsc -w" + }, + "keywords": [ + "opentelemetry", + "web", + "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", + "@types/zone.js": "^0.5.12", + "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", + "tslint-consistent-codestyle": "^1.16.0", + "tslint-microsoft-contrib": "^6.2.0", + "ts-loader": "^6.0.4", + "ts-mocha": "^6.0.0", + "ts-node": "^8.0.0", + "typescript": "^3.6.3", + "webpack": "^4.35.2", + "webpack-cli": "^3.3.9", + "zone.js": "^0.10.2" + }, + "dependencies": { + "@opentelemetry/scope-base": "^0.2.0" + }, + "peerDependencies": { + "zone.js": "^0.10.2" + }, + "sideEffects": false +} diff --git a/packages/opentelemetry-scope-zone-peer-dep/src/ZoneScopeManager.ts b/packages/opentelemetry-scope-zone-peer-dep/src/ZoneScopeManager.ts new file mode 100644 index 00000000000..d735e73c7d5 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/src/ZoneScopeManager.ts @@ -0,0 +1,258 @@ +/*! + * 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 { ScopeManager } from '@opentelemetry/scope-base'; +import { Func, TargetWithEvents } from './types'; +import { isListenerObject } from './util'; + +/* Key name to be used to save a scope reference in Zone */ +const ZONE_SCOPE_KEY = 'OT_ZONE_SCOPE'; + +/** + * ZoneScopeManager + * This module provides an easy functionality for tracing action between asynchronous operations in web. + * It was not possible with standard [StackScopeManager]{@link https://github.com/open-telemetry/opentelemetry-js/blob/master/packages/opentelemetry-web/src/StackScopeManager.ts}. + * It heavily depends on [zone.js]{@link https://www.npmjs.com/package/zone.js}. + * It stores the information about scope in zone. Each Scope will have always new Zone; + * It also supports binding a certain Span to a target that has "addEventListener" and "removeEventListener". + * When this happens a new zone is being created and the provided Span is being assigned to this zone. + */ +export class ZoneScopeManager implements ScopeManager { + /** + * whether the scope manager is enabled or not + */ + private _enabled = false; + + /** + * Helps to create a unique name for the zones - part of zone name + */ + private _zoneCounter = 0; + + /** + * Returns the active scope from certain zone name + * @param activeZone + */ + private _activeScopeFromZone( + activeZone: Zone | undefined + ): unknown | undefined { + return activeZone && activeZone.get(ZONE_SCOPE_KEY); + } + + /** + * @param target Function to be executed within the scope + * @param scope A scope (span) to be executed within target function + */ + private _bindFunction(target: T, scope?: unknown): T { + const manager = this; + const contextWrapper = function(this: any, ...args: unknown[]) { + return manager.with(scope, () => target.apply(this || scope, args)); + }; + Object.defineProperty(contextWrapper, 'length', { + enumerable: false, + configurable: true, + writable: false, + value: target.length, + }); + return (contextWrapper as unknown) as T; + } + + /** + * @param obj target object on which the listeners will be patched + * @param scope A scope (span) to be bind to target + */ + private _bindListener(obj: T, scope?: unknown): T { + const target = (obj as unknown) as TargetWithEvents; + if (target.__ot_listeners !== undefined) { + return obj; + } + target.__ot_listeners = {}; + + if (typeof target.addEventListener === 'function') { + target.addEventListener = this._patchAddEventListener( + target, + target.addEventListener, + scope + ); + } + + if (typeof target.removeEventListener === 'function') { + target.removeEventListener = this._patchRemoveEventListener( + target, + target.removeEventListener + ); + } + + return obj; + } + + /** + * Creates a new unique zone name + */ + private _createZoneName() { + this._zoneCounter++; + const random = Math.random(); + return `${this._zoneCounter}-${random}`; + } + + /** + * Creates a new zone + * @param zoneName zone name + * @param scope A scope (span) to be bind with Zone + */ + private _createZone(zoneName: string, scope: unknown): Zone { + return Zone.root.fork({ + name: zoneName, + properties: { + [ZONE_SCOPE_KEY]: scope, + }, + }); + } + + /** + * Returns the active zone + */ + private _getActiveZone(): Zone | undefined { + return Zone.current; + } + + /** + * Patches addEventListener method + * @param target any target that has "addEventListener" method + * @param original reference to the patched method + * @param [scope] scope to be bind to the listener + */ + private _patchAddEventListener( + target: TargetWithEvents, + original: Function, + scope?: unknown + ) { + const scopeManager = this; + + return function(this: {}, event: string, listener: Func, opts?: any) { + if (target.__ot_listeners === undefined) { + target.__ot_listeners = {}; + } + let listeners = target.__ot_listeners[event]; + if (listeners === undefined) { + listeners = new WeakMap(); + target.__ot_listeners[event] = listeners; + } + const patchedListener = scopeManager.bind(listener, scope); + // store a weak reference of the user listener to ours + listeners.set(listener, patchedListener); + return original.call(this, event, patchedListener, opts); + }; + } + + /** + * Patches removeEventListener method + * @param target any target that has "removeEventListener" method + * @param original reference to the patched method + */ + private _patchRemoveEventListener( + target: TargetWithEvents, + original: Function + ) { + return function(this: {}, event: string, listener: Func) { + if ( + target.__ot_listeners === undefined || + target.__ot_listeners[event] === undefined + ) { + return original.call(this, event, listener); + } + const events = target.__ot_listeners[event]; + const patchedListener = events.get(listener); + events.delete(listener); + return original.call(this, event, patchedListener || listener); + }; + } + + /** + * Returns the active scope + */ + active(): unknown | undefined { + const activeZone = this._getActiveZone(); + + const active = this._activeScopeFromZone(activeZone); + if (active) { + return active; + } + if (this._enabled) { + return window; + } + return undefined; + } + + /** + * Binds a the certain scope or the active one to the target function and then returns the target + * @param target + * @param scope A scope (span) to be bind to target + */ + bind(target: T | TargetWithEvents, scope?: unknown): T { + // if no specific scope to propagate is given, we use the current one + if (scope === undefined) { + scope = this.active(); + } + if (typeof target === 'function') { + return this._bindFunction(target, scope); + } else if (isListenerObject(target)) { + this._bindListener(target, scope); + } + return (target as unknown) as T; + } + + /** + * Disable the scope manager (clears all the scopes) + */ + disable(): this { + this._enabled = false; + return this; + } + + /** + * Enables the scope manager and creates a default(root) scope + */ + enable(): this { + if (this._enabled) { + return this; + } + this._enabled = true; + return this; + } + + /** + * Calls the callback function [fn] with the provided [scope]. + * If [scope] is undefined then it will use the active scope. + * The scope will be set as active + * @param scope A scope (span) to be called with provided callback + * @param fn Callback function + */ + with ReturnType>( + scope: unknown, + fn: () => ReturnType + ): ReturnType { + // if no scope use active from active zone + if (typeof scope === 'undefined' || scope === null) { + scope = this.active(); + } + + const zoneName = this._createZoneName(); + + const newZone = this._createZone(zoneName, scope); + + return newZone.run(fn, scope); + } +} diff --git a/packages/opentelemetry-scope-zone-peer-dep/src/index.ts b/packages/opentelemetry-scope-zone-peer-dep/src/index.ts new file mode 100644 index 00000000000..3360b42ee33 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/src/index.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. + */ + +export * from './ZoneScopeManager'; +export * from './types'; diff --git a/packages/opentelemetry-scope-zone-peer-dep/src/types.ts b/packages/opentelemetry-scope-zone-peer-dep/src/types.ts new file mode 100644 index 00000000000..e67614be708 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/src/types.ts @@ -0,0 +1,35 @@ +/*! + * 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 type Func = (...args: unknown[]) => T; + +/** + * Minimum requirements that the object needs to have so that it can bind to the events instead of function + * this is "addEventListener" and "removeEventListener" - see {@link isListenerObject} + */ +export interface TargetWithEvents { + addEventListener?( + event: string, + listener: (...args: any[]) => void, + opts?: { once: boolean } + ): any; + removeEventListener?( + event: string, + listener: (...args: any[]) => void, + opts?: { once: boolean } + ): any; + __ot_listeners?: { [name: string]: WeakMap, Func> }; +} diff --git a/packages/opentelemetry-scope-zone-peer-dep/src/util.ts b/packages/opentelemetry-scope-zone-peer-dep/src/util.ts new file mode 100644 index 00000000000..98022f92195 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/src/util.ts @@ -0,0 +1,28 @@ +/*! + * 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 { TargetWithEvents } from './types'; + +/** + * check if an object has addEventListener and removeEventListener functions then it will return true + * @param obj + */ +export function isListenerObject(obj: TargetWithEvents = {}): boolean { + return ( + typeof obj.addEventListener === 'function' && + typeof obj.removeEventListener === 'function' + ); +} diff --git a/packages/opentelemetry-scope-zone-peer-dep/test/ZoneScopeManager.test.ts b/packages/opentelemetry-scope-zone-peer-dep/test/ZoneScopeManager.test.ts new file mode 100644 index 00000000000..490d5bfe455 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/test/ZoneScopeManager.test.ts @@ -0,0 +1,290 @@ +/*! + * 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 'zone.js'; +import * as sinon from 'sinon'; +import * as assert from 'assert'; +import { ZoneScopeManager } from '../src'; + +let clock: any; + +describe('ZoneScopeManager', () => { + let scopeManager: ZoneScopeManager; + + beforeEach(() => { + clock = sinon.useFakeTimers(); + scopeManager = new ZoneScopeManager(); + scopeManager.enable(); + }); + + afterEach(() => { + clock.restore(); + scopeManager.disable(); + }); + + describe('.enable()', () => { + it('should work', () => { + assert.doesNotThrow(() => { + assert(scopeManager.enable() === scopeManager, 'should return this'); + assert(scopeManager.active() === window, 'should has root scope'); + }); + }); + }); + + describe('.disable()', () => { + it('should work', () => { + assert.doesNotThrow(() => { + assert(scopeManager.disable() === scopeManager, 'should return this'); + assert(scopeManager.active() === undefined, 'should has no scope'); + }); + }); + }); + + describe('.with()', () => { + it('should run the callback (null as target)', done => { + scopeManager.with(null, done); + }); + + it('should run the callback (object as target)', done => { + const test = { a: 1 }; + scopeManager.with(test, () => { + assert.strictEqual(scopeManager.active(), test, 'should have scope'); + return done(); + }); + }); + + it('should run the callback (when disabled)', done => { + scopeManager.disable(); + scopeManager.with(null, () => { + scopeManager.enable(); + return done(); + }); + }); + + it('should rethrow errors', done => { + assert.throws(() => { + scopeManager.with(null, () => { + throw new Error('This should be rethrown'); + }); + }); + return done(); + }); + + it('should finally restore an old scope, including the async task', done => { + const scope1 = 'scope1'; + const scope2 = 'scope2'; + const scope3 = 'scope3'; + + scopeManager.with(scope1, () => { + assert.strictEqual(scopeManager.active(), 'scope1'); + scopeManager.with(scope2, () => { + assert.strictEqual(scopeManager.active(), 'scope2'); + scopeManager.with(scope3, () => { + assert.strictEqual(scopeManager.active(), 'scope3'); + }); + assert.strictEqual(scopeManager.active(), 'scope2'); + }); + assert.strictEqual(scopeManager.active(), 'scope1'); + setTimeout(() => { + assert.strictEqual(scopeManager.active(), 'scope1'); + done(); + }, 500); + clock.tick(500); + }); + assert.strictEqual(scopeManager.active(), window); + }); + + it('should finally restore an old scope when scope is an object, including the async task', done => { + const scope1 = { a: 1 }; + const scope2 = { a: 2 }; + const scope3 = { a: 3 }; + scopeManager.with(scope1, () => { + assert.strictEqual(scopeManager.active(), scope1); + scopeManager.with(scope2, () => { + assert.strictEqual(scopeManager.active(), scope2); + scopeManager.with(scope3, () => { + assert.strictEqual(scopeManager.active(), scope3); + }); + assert.strictEqual(scopeManager.active(), scope2); + }); + assert.strictEqual(scopeManager.active(), scope1); + setTimeout(() => { + assert.strictEqual(scopeManager.active(), scope1); + done(); + }, 500); + clock.tick(500); + }); + assert.strictEqual(scopeManager.active(), window); + }); + it('should correctly return the scopes for 3 parallel actions', () => { + const rootSpan = { name: 'rootSpan' }; + scopeManager.with(rootSpan, () => { + assert.ok( + scopeManager.active() === rootSpan, + 'Current span is rootSpan' + ); + const concurrentSpan1 = { name: 'concurrentSpan1' }; + const concurrentSpan2 = { name: 'concurrentSpan2' }; + const concurrentSpan3 = { name: 'concurrentSpan3' }; + + scopeManager.with(concurrentSpan1, () => { + setTimeout(() => { + assert.ok( + scopeManager.active() === concurrentSpan1, + 'Current span is concurrentSpan1' + ); + }, 10); + }); + + scopeManager.with(concurrentSpan2, () => { + setTimeout(() => { + assert.ok( + scopeManager.active() === concurrentSpan2, + 'Current span is concurrentSpan2' + ); + }, 20); + }); + + scopeManager.with(concurrentSpan3, () => { + setTimeout(() => { + assert.ok( + scopeManager.active() === concurrentSpan3, + 'Current span is concurrentSpan3' + ); + }, 30); + }); + }); + }); + }); + + describe('.bind(function)', () => { + it('should call the function with previously assigned scope', () => { + class Obj { + title: string; + + constructor(title: string) { + this.title = title; + } + + getTitle() { + return this.title; + } + } + + const obj1 = new Obj('a1'); + obj1.title = 'a2'; + const obj2 = new Obj('b1'); + const wrapper: any = scopeManager.bind(obj2.getTitle, obj1); + assert.ok(wrapper(), 'a2'); + }); + + it('should return the same target (when enabled)', () => { + const test = { a: 1 }; + assert.deepStrictEqual(scopeManager.bind(test), test); + }); + + it('should return the same target (when disabled)', () => { + scopeManager.disable(); + const test = { a: 1 }; + assert.deepStrictEqual(scopeManager.bind(test), test); + scopeManager.enable(); + }); + + it('should return current scope (when enabled)', done => { + const scope = { a: 1 }; + const fn: any = scopeManager.bind(() => { + assert.strictEqual(scopeManager.active(), scope, 'should have scope'); + return done(); + }, scope); + fn(); + }); + + it('should return current scope (when disabled)', done => { + scopeManager.disable(); + const scope = { a: 1 }; + const fn: any = scopeManager.bind(() => { + assert.strictEqual(scopeManager.active(), scope, 'should have scope'); + return done(); + }, scope); + fn(); + }); + + it('should bind the the certain scope to the target "addEventListener" function', done => { + const scope1 = { a: 1 }; + const element = document.createElement('div'); + + scopeManager.bind(element, scope1); + + element.addEventListener('click', () => { + assert.strictEqual(scopeManager.active(), scope1); + setTimeout(() => { + assert.strictEqual(scopeManager.active(), scope1); + done(); + }, 500); + clock.tick(500); + }); + + element.dispatchEvent( + new CustomEvent('click', { + bubbles: true, + cancelable: false, + composed: true, + }) + ); + }); + + it('should preserve zone when creating new click event inside zone', done => { + const scope1 = { a: 1 }; + const element = document.createElement('div'); + + scopeManager.bind(element, scope1); + + element.addEventListener('click', () => { + assert.strictEqual(scopeManager.active(), scope1); + setTimeout(() => { + assert.strictEqual(scopeManager.active(), scope1); + const element2 = document.createElement('div'); + + element2.addEventListener('click', () => { + assert.strictEqual(scopeManager.active(), scope1); + setTimeout(() => { + assert.strictEqual(scopeManager.active(), scope1); + done(); + }, 500); + clock.tick(500); + }); + + element2.dispatchEvent( + new CustomEvent('click', { + bubbles: true, + cancelable: false, + composed: true, + }) + ); + }, 500); + clock.tick(500); + }); + + element.dispatchEvent( + new CustomEvent('click', { + bubbles: true, + cancelable: false, + composed: true, + }) + ); + }); + }); +}); diff --git a/packages/opentelemetry-scope-zone-peer-dep/test/index-webpack.ts b/packages/opentelemetry-scope-zone-peer-dep/test/index-webpack.ts new file mode 100644 index 00000000000..7731f090914 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/test/index-webpack.ts @@ -0,0 +1,23 @@ +/*! + * 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('.', true, /test$/); +testsContext.keys().forEach(testsContext); + +const srcContext = require.context('.', true, /src$/); +srcContext.keys().forEach(srcContext); diff --git a/packages/opentelemetry-scope-zone-peer-dep/test/utils.test.ts b/packages/opentelemetry-scope-zone-peer-dep/test/utils.test.ts new file mode 100644 index 00000000000..790bbb28fbe --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/test/utils.test.ts @@ -0,0 +1,54 @@ +/*! + * 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 * as utils from '../src/util'; + +describe('ZoneScopeManager utils', () => { + describe('isListenerObject', () => { + describe('when object contains "addEventListener" and "removeEventListener"', () => { + it('should return true', () => { + const obj = { + addEventListener: function() {}, + removeEventListener: function() {}, + }; + assert.strictEqual(utils.isListenerObject(obj), true); + }); + }); + describe('when object doesn\'t contain "addEventListener" and "removeEventListener"', () => { + it('should return true', () => { + const obj = {}; + assert.strictEqual(utils.isListenerObject(obj), false); + }); + }); + describe('when object contains "addEventListener" only', () => { + it('should return false', () => { + const obj = { + addEventListener: function() {}, + }; + assert.strictEqual(utils.isListenerObject(obj), false); + }); + }); + describe('when object contains "removeEventListener" only', () => { + it('should return false', () => { + const obj = { + removeEventListener: function() {}, + }; + assert.strictEqual(utils.isListenerObject(obj), false); + }); + }); + }); +}); diff --git a/packages/opentelemetry-scope-zone-peer-dep/tsconfig.json b/packages/opentelemetry-scope-zone-peer-dep/tsconfig.json new file mode 100644 index 00000000000..ab49dd3fbd6 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "files": [ "node_modules/zone.js/dist/zone.js.d.ts"], + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/packages/opentelemetry-scope-zone-peer-dep/tslint.json b/packages/opentelemetry-scope-zone-peer-dep/tslint.json new file mode 100644 index 00000000000..0710b135d07 --- /dev/null +++ b/packages/opentelemetry-scope-zone-peer-dep/tslint.json @@ -0,0 +1,4 @@ +{ + "rulesDirectory": ["node_modules/tslint-microsoft-contrib"], + "extends": ["../../tslint.base.js", "./node_modules/tslint-consistent-codestyle"] +} diff --git a/packages/opentelemetry-scope-zone/LICENSE b/packages/opentelemetry-scope-zone/LICENSE new file mode 100644 index 00000000000..261eeb9e9f8 --- /dev/null +++ b/packages/opentelemetry-scope-zone/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-scope-zone/README.md b/packages/opentelemetry-scope-zone/README.md new file mode 100644 index 00000000000..3ab2fd1f1c4 --- /dev/null +++ b/packages/opentelemetry-scope-zone/README.md @@ -0,0 +1,71 @@ +# OpenTelemetry Scope Zone +[![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 *Zone Scope Manager with bundled [zone-js]* for Web applications. +If you have your own [zone-js] please use [@opentelemetry/scope-zone-peer-dep] +If you use Angular it means you already have the [zone-js] and you should use [@opentelemetry/scope-zone-peer-dep] + + +## Installation + +```bash +npm install --save @opentelemetry/scope-zone +``` + +## Usage +```js +import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; +import { WebTracer } from '@opentelemetry/web'; +import { ZoneScopeManager } from '@opentelemetry/scope-zone'; + +const webTracerWithZone = new WebTracer({ + scopeManager: new ZoneScopeManager() +}); +webTracerWithZone.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); + +// Example how the ZoneScopeManager keeps the reference to the correct scope during async operations +const span1 = webTracerWithZone.startSpan('foo1'); +webTracerWithZone.withSpan(span1, () => { + console.log('Current span is span1', webTracerWithZone.getCurrentSpan() === span1); + setTimeout(() => { + const span2 = webTracerWithZone.startSpan('foo2'); + console.log('Current span is span1', webTracerWithZone.getCurrentSpan() === span1); + webTracerWithZone.withSpan(span2, () => { + console.log('Current span is span2', webTracerWithZone.getCurrentSpan() === span2); + setTimeout(() => { + console.log('Current span is span2', webTracerWithZone.getCurrentSpan() === span2); + }, 500); + }); + // there is a timeout which still keeps span2 active + console.log('Current span is span2', webTracerWithZone.getCurrentSpan() === span2); + }, 500); + console.log('Current span is span1', webTracerWithZone.getCurrentSpan() === span1); +}); + +``` + +## 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-scope-zone +[dependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-scope-zone +[devDependencies-image]: https://david-dm.org/open-telemetry/opentelemetry-js/dev-status.svg?path=packages/opentelemetry-scope-zone +[devDependencies-url]: https://david-dm.org/open-telemetry/opentelemetry-js?path=packages%2Fopentelemetry-web&type=dev +[npm-url]: https://www.npmjs.com/package/@opentelemetry/scope-zone +[npm-img]: https://badge.fury.io/js/%40opentelemetry%2Fscope-zone.svg +[zone-js]: https://www.npmjs.com/package/zone.js +[@opentelemetry/scope-zone-peer-dep]: https://www.npmjs.com/package/@opentelemetry/scope-zone-peer-dep diff --git a/packages/opentelemetry-scope-zone/package.json b/packages/opentelemetry-scope-zone/package.json new file mode 100644 index 00000000000..537571c6a45 --- /dev/null +++ b/packages/opentelemetry-scope-zone/package.json @@ -0,0 +1,73 @@ +{ + "name": "@opentelemetry/scope-zone", + "version": "0.2.0", + "description": "OpenTelemetry Scope Zone", + "main": "build/src/index.js", + "types": "build/src/index.d.ts", + "repository": "open-telemetry/opentelemetry-js", + "scripts": { + "check": "gts check", + "clean": "rimraf build/*", + "compile": "tsc -p .", + "fix": "gts fix", + "prepare": "npm run compile", + "watch": "tsc -w" + }, + "keywords": [ + "opentelemetry", + "web", + "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": { + "@types/mocha": "^5.2.5", + "@types/node": "^12.6.8", + "@types/webpack-env": "1.13.9", + "@types/sinon": "^7.0.13", + "@babel/core": "^7.6.0", + "babel-loader": "^8.0.6", + "codecov": "^3.1.0", + "gts": "^1.0.0", + "karma": "^4.1.0", + "karma-chrome-launcher": "^2.2.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", + "tslint-consistent-codestyle": "^1.16.0", + "tslint-microsoft-contrib": "^6.2.0", + "ts-loader": "^6.0.4", + "ts-mocha": "^6.0.0", + "ts-node": "^8.0.0", + "typescript": "^3.6.3", + "webpack": "^4.35.2", + "webpack-cli": "^3.3.9", + "webpack-merge": "^4.2.2" + }, + "dependencies": { + "@opentelemetry/scope-zone-peer-dep": "^0.2.0", + "zone.js": "^0.10.2" + }, + + "sideEffects": true +} diff --git a/packages/opentelemetry-scope-zone/src/index.ts b/packages/opentelemetry-scope-zone/src/index.ts new file mode 100644 index 00000000000..425ddebfda3 --- /dev/null +++ b/packages/opentelemetry-scope-zone/src/index.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. + */ + +export * from '@opentelemetry/scope-zone-peer-dep'; +import 'zone.js'; diff --git a/packages/opentelemetry-scope-zone/tsconfig.json b/packages/opentelemetry-scope-zone/tsconfig.json new file mode 100644 index 00000000000..ab49dd3fbd6 --- /dev/null +++ b/packages/opentelemetry-scope-zone/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.base", + "compilerOptions": { + "rootDir": ".", + "outDir": "build" + }, + "files": [ "node_modules/zone.js/dist/zone.js.d.ts"], + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/packages/opentelemetry-scope-zone/tslint.json b/packages/opentelemetry-scope-zone/tslint.json new file mode 100644 index 00000000000..0710b135d07 --- /dev/null +++ b/packages/opentelemetry-scope-zone/tslint.json @@ -0,0 +1,4 @@ +{ + "rulesDirectory": ["node_modules/tslint-microsoft-contrib"], + "extends": ["../../tslint.base.js", "./node_modules/tslint-consistent-codestyle"] +} diff --git a/packages/opentelemetry-web/README.md b/packages/opentelemetry-web/README.md index 20117c68fce..54e6a8d94cc 100644 --- a/packages/opentelemetry-web/README.md +++ b/packages/opentelemetry-web/README.md @@ -8,14 +8,36 @@ This module provides *automated instrumentation and tracing* for Web applications. For manual instrumentation see the -[@opentelemetry/web](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-web) package. +[@opentelemetry/tracing](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-tracing) package. ## How does automatic tracing work? +This package exposes a class `WebTracer` that will be able to automatically trace things in Browser only. + +See the example how to use it. + +OpenTelemetry comes with a growing number of instrumentation plugins for well know modules (see [supported modules](https://github.com/open-telemetry/opentelemetry-js#plugins)) and an API to create custom plugins (see [the plugin developer guide](https://github.com/open-telemetry/opentelemetry-js/blob/master/doc/plugin-guide.md)). + +Web Tracer currently supports one plugin for document load. +Unlike Node Tracer, the plugins needs to be initialised and passed in configuration. +The reason is to give user full control over which plugin will be bundled into web page. + +You can choose to use the ZoneScopeManager if you want to trace asynchronous operations. + +## Installation + +```bash +npm install --save @opentelemetry/web +``` + +## Usage + ```js import { ConsoleSpanExporter, SimpleSpanProcessor } from '@opentelemetry/tracing'; import { WebTracer } from '@opentelemetry/web'; import { DocumentLoad } from '@opentelemetry/plugin-document-load'; +import { ZoneScopeManager } from '@opentelemetry/scope-zone'; +// Minimum required setup - supports only synchronous operations const webTracer = new WebTracer({ plugins: [ new DocumentLoad() @@ -23,26 +45,15 @@ const webTracer = new WebTracer({ }); webTracer.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); -``` - -## Installation - -```bash -npm install --save @opentelemetry/web -``` -## Usage - -```js -// Manual -const { WebTracer } = require('@opentelemetry/web'); -const webTracer = new WebTracer(); -const span = webTracer.startSpan('span1'); -webTracer.withSpan(span, function () { - this.addEvent('start'); +// Changing default scopeManager to use ZoneScopeManager - supports asynchronous operations +const webTracerWithZone = new WebTracer({ + scopeManager: new ZoneScopeManager(), + plugins: [ + new DocumentLoad() + ] }); -span.addEvent('middle'); -span.end(); +webTracerWithZone.addSpanProcessor(new SimpleSpanProcessor(new ConsoleSpanExporter())); ``` diff --git a/packages/opentelemetry-web/package.json b/packages/opentelemetry-web/package.json index e8ae37fa6c0..188f0df5b37 100644 --- a/packages/opentelemetry-web/package.json +++ b/packages/opentelemetry-web/package.json @@ -68,7 +68,8 @@ "typescript": "3.7.2", "webpack": "^4.35.2", "webpack-cli": "^3.3.9", - "webpack-merge": "^4.2.2" + "webpack-merge": "^4.2.2", + "@opentelemetry/scope-zone": "^0.2.0" }, "dependencies": { "@opentelemetry/core": "^0.2.0", diff --git a/packages/opentelemetry-web/test/WebTracer.test.ts b/packages/opentelemetry-web/test/WebTracer.test.ts index e1474508ea7..5aa3a5a8590 100644 --- a/packages/opentelemetry-web/test/WebTracer.test.ts +++ b/packages/opentelemetry-web/test/WebTracer.test.ts @@ -21,6 +21,7 @@ import { BasicTracerConfig } from '@opentelemetry/tracing'; import { WebTracerConfig } from '../src'; import { StackScopeManager } from '../src/StackScopeManager'; import { WebTracer } from '../src/WebTracer'; +import { ZoneScopeManager } from '@opentelemetry/scope-zone'; class DummyPlugin extends BasePlugin { patch() {} @@ -78,5 +79,46 @@ describe('WebTracer', () => { tracer = new WebTracer({}); }); }); + + describe('when scopeManager is "ZoneScopeManager"', () => { + it('should correctly return the scopes for 2 parallel actions', () => { + const webTracerWithZone = new WebTracer({ + scopeManager: new ZoneScopeManager(), + }); + + const rootSpan = webTracerWithZone.startSpan('rootSpan'); + + webTracerWithZone.withSpan(rootSpan, () => { + assert.ok( + webTracerWithZone.getCurrentSpan() === rootSpan, + 'Current span is rootSpan' + ); + const concurrentSpan1 = webTracerWithZone.startSpan( + 'concurrentSpan1' + ); + const concurrentSpan2 = webTracerWithZone.startSpan( + 'concurrentSpan2' + ); + + webTracerWithZone.withSpan(concurrentSpan1, () => { + setTimeout(() => { + assert.ok( + webTracerWithZone.getCurrentSpan() === concurrentSpan1, + 'Current span is concurrentSpan1' + ); + }, 10); + }); + + webTracerWithZone.withSpan(concurrentSpan2, () => { + setTimeout(() => { + assert.ok( + webTracerWithZone.getCurrentSpan() === concurrentSpan2, + 'Current span is concurrentSpan2' + ); + }, 20); + }); + }); + }); + }); }); }); From 55f7d5f5f0adcf028e2d75935814e1d2612849f7 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Mon, 11 Nov 2019 09:32:42 -0800 Subject: [PATCH 06/17] chore: minor name change (#512) --- packages/opentelemetry-types/README.md | 2 +- packages/opentelemetry-types/tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/opentelemetry-types/README.md b/packages/opentelemetry-types/README.md index 204dddf4a72..16a4ce769c8 100644 --- a/packages/opentelemetry-types/README.md +++ b/packages/opentelemetry-types/README.md @@ -1,4 +1,4 @@ -# OpenTelemetry Types +# OpenTelemetry API for JavaScript [![Gitter chat][gitter-image]][gitter-url] [![NPM Published Version][npm-img]][npm-url] [![dependencies][dependencies-image]][dependencies-url] diff --git a/packages/opentelemetry-types/tsconfig.json b/packages/opentelemetry-types/tsconfig.json index ee1507d1ad0..2b05ed8413a 100644 --- a/packages/opentelemetry-types/tsconfig.json +++ b/packages/opentelemetry-types/tsconfig.json @@ -8,7 +8,7 @@ "src/**/*.ts" ], "typedocOptions": { - "name": "OpenTelemetry Documentation", + "name": "OpenTelemetry API for JavaScript", "out": "docs/out", "mode": "file", "hideGenerator": true From fcb0f1aa475ae168c882d0f326919566358fa01b Mon Sep 17 00:00:00 2001 From: Daniel Dyla Date: Mon, 11 Nov 2019 12:48:08 -0500 Subject: [PATCH 07/17] fix: add missing dev dependencies (#516) * fix: add missing dev dependencies * fix: lint --- packages/opentelemetry-core/package.json | 1 + packages/opentelemetry-plugin-grpc/package.json | 1 + 2 files changed, 2 insertions(+) diff --git a/packages/opentelemetry-core/package.json b/packages/opentelemetry-core/package.json index b1b552552f2..11a6a47364f 100644 --- a/packages/opentelemetry-core/package.json +++ b/packages/opentelemetry-core/package.json @@ -51,6 +51,7 @@ "devDependencies": { "@types/mocha": "^5.2.5", "@types/node": "^12.6.8", + "@types/semver": "^6.2.0", "@types/sinon": "^7.0.13", "@types/webpack-env": "1.13.9", "codecov": "^3.1.0", diff --git a/packages/opentelemetry-plugin-grpc/package.json b/packages/opentelemetry-plugin-grpc/package.json index 52d873c9691..9e14f27d0b5 100644 --- a/packages/opentelemetry-plugin-grpc/package.json +++ b/packages/opentelemetry-plugin-grpc/package.json @@ -44,6 +44,7 @@ "@opentelemetry/tracing": "^0.2.0", "@types/mocha": "^5.2.7", "@types/node": "^12.6.9", + "@types/semver": "^6.2.0", "@types/shimmer": "^1.0.1", "@types/sinon": "^7.0.13", "codecov": "^3.5.0", From 39732fabe85b1bca064b91c3d84a101998e71d1b Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Mon, 11 Nov 2019 10:06:45 -0800 Subject: [PATCH 08/17] feat: add ConsoleMetricExporter (#500) * feat: add ConsoleMetricExporter * fix: review comments * fix: linting --- packages/opentelemetry-metrics/package.json | 2 + .../src/export/ConsoleMetricExporter.ts | 58 ++++++++++++++++++ packages/opentelemetry-metrics/src/index.ts | 1 + .../test/export/ConsoleMetricExporter.test.ts | 61 +++++++++++++++++++ 4 files changed, 122 insertions(+) create mode 100644 packages/opentelemetry-metrics/src/export/ConsoleMetricExporter.ts create mode 100644 packages/opentelemetry-metrics/test/export/ConsoleMetricExporter.test.ts diff --git a/packages/opentelemetry-metrics/package.json b/packages/opentelemetry-metrics/package.json index ebf791c0a2d..a7db5104296 100644 --- a/packages/opentelemetry-metrics/package.json +++ b/packages/opentelemetry-metrics/package.json @@ -41,11 +41,13 @@ "devDependencies": { "@types/mocha": "^5.2.7", "@types/node": "^12.6.9", + "@types/sinon": "^7.0.13", "codecov": "^3.5.0", "gts": "^1.1.0", "mocha": "^6.2.0", "nyc": "^14.1.1", "rimraf": "^3.0.0", + "sinon": "^7.5.0", "ts-mocha": "^6.0.0", "ts-node": "^8.3.0", "tslint-consistent-codestyle": "^1.15.1", diff --git a/packages/opentelemetry-metrics/src/export/ConsoleMetricExporter.ts b/packages/opentelemetry-metrics/src/export/ConsoleMetricExporter.ts new file mode 100644 index 00000000000..5702d7258b7 --- /dev/null +++ b/packages/opentelemetry-metrics/src/export/ConsoleMetricExporter.ts @@ -0,0 +1,58 @@ +/*! + * 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 { MetricExporter, ReadableMetric } from './types'; +import { ExportResult } from '@opentelemetry/base'; + +/** + * This is implementation of {@link MetricExporter} that prints metrics data to + * the console. This class can be used for diagnostic purposes. + */ +export class ConsoleMetricExporter implements MetricExporter { + export( + metrics: ReadableMetric[], + resultCallback: (result: ExportResult) => void + ): void { + for (const metric of metrics) { + const descriptor = metric.descriptor; + const timeseries = metric.timeseries; + console.log({ + name: descriptor.name, + description: descriptor.description, + }); + + for (const ts of timeseries) { + const labels = descriptor.labelKeys + .map((k, i) => [k, ts.labelValues[i]]) + .reduce( + (p, c) => ({ + ...p, + [c[0] as string]: typeof c[1] === 'string' ? c[1] : c[1].value, + }), + {} + ); + for (const point of ts.points) { + console.log({ labels, value: point.value }); + } + } + } + return resultCallback(ExportResult.SUCCESS); + } + + shutdown(): void { + // By default does nothing + } +} diff --git a/packages/opentelemetry-metrics/src/index.ts b/packages/opentelemetry-metrics/src/index.ts index 6c1d1c23b9c..71f30b58c44 100644 --- a/packages/opentelemetry-metrics/src/index.ts +++ b/packages/opentelemetry-metrics/src/index.ts @@ -17,4 +17,5 @@ export * from './Handle'; export * from './Meter'; export * from './Metric'; +export * from './export/ConsoleMetricExporter'; export * from './export/types'; diff --git a/packages/opentelemetry-metrics/test/export/ConsoleMetricExporter.test.ts b/packages/opentelemetry-metrics/test/export/ConsoleMetricExporter.test.ts new file mode 100644 index 00000000000..ac4d12794d5 --- /dev/null +++ b/packages/opentelemetry-metrics/test/export/ConsoleMetricExporter.test.ts @@ -0,0 +1,61 @@ +/*! + * 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 * as sinon from 'sinon'; +import { ConsoleMetricExporter, Meter, GaugeMetric } from '../../src'; + +describe('ConsoleMetricExporter', () => { + let consoleExporter: ConsoleMetricExporter; + let previousConsoleLog: any; + + beforeEach(() => { + previousConsoleLog = console.log; + console.log = () => {}; + consoleExporter = new ConsoleMetricExporter(); + }); + + afterEach(() => { + console.log = previousConsoleLog; + }); + + describe('.export()', () => { + it('should export information about metrics', () => { + const spyConsole = sinon.spy(console, 'log'); + + const meter = new Meter(); + meter.addExporter(consoleExporter); + const gauge = meter.createGauge('gauge', { + description: 'a test description', + labelKeys: ['key1', 'key2'], + }) as GaugeMetric; + const handle = gauge.getHandle( + meter.labels({ key1: 'labelValue1', key2: 'labelValue2' }) + ); + handle.set(10); + const [descriptor, timeseries] = spyConsole.args; + assert.deepStrictEqual(descriptor, [ + { description: 'a test description', name: 'gauge' }, + ]); + assert.deepStrictEqual(timeseries, [ + { + labels: { key1: 'labelValue1', key2: 'labelValue2' }, + value: 10, + }, + ]); + }); + }); +}); From 460d986f6739d5b89993987f98a39d16a19ea8b4 Mon Sep 17 00:00:00 2001 From: Daniel Dyla Date: Mon, 11 Nov 2019 17:02:55 -0500 Subject: [PATCH 09/17] feat: Add prometheus exporter (#483) * fix(http-plugin): move node-sdk to dev deps * feat: update dependencies * feat: server implementation and tests * fix: remove console logger * fix: add types * feat(prometheus-exporter): counter and gauge * feat(prometheus-exporter): implement counter and gauge * feat(prometheus-exporter): export configuration interface * fix: linting * fix: typo Co-Authored-By: Mayur Kale * chore: document ExporterConfig * fix: dependencies * fix: typo Co-Authored-By: Mayur Kale * chore: document sanitize method and make behavior more strict * chore: do not use global prom-client registry * chore: add defaults to description and metric endpoint * test: export couter, gauge, multiple, and none * test: sanitize prom metric names * fix: typo Co-Authored-By: Mayur Kale * chore: make _server read only * test: add tests for prom export configuration * style: make comments descriptive * test: update tests for label sets * fix: lint --- .../package.json | 9 +- .../src/export/types.ts | 53 ++ .../src/index.ts | 3 +- .../src/prometheus.ts | 309 ++++++++++++ .../test/prometheus.test.ts | 464 ++++++++++++++++++ .../tslint.json | 4 + 6 files changed, 838 insertions(+), 4 deletions(-) create mode 100644 packages/opentelemetry-exporter-prometheus/src/export/types.ts create mode 100644 packages/opentelemetry-exporter-prometheus/src/prometheus.ts create mode 100644 packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts create mode 100644 packages/opentelemetry-exporter-prometheus/tslint.json diff --git a/packages/opentelemetry-exporter-prometheus/package.json b/packages/opentelemetry-exporter-prometheus/package.json index 990828e5f28..4361f1bf519 100644 --- a/packages/opentelemetry-exporter-prometheus/package.json +++ b/packages/opentelemetry-exporter-prometheus/package.json @@ -1,7 +1,6 @@ { "name": "@opentelemetry/exporter-prometheus", "version": "0.2.0", - "private": true, "description": "OpenTelemetry Exporter Prometheus provides a metrics endpoint for Prometheus", "main": "build/src/index.js", "types": "build/src/index.d.ts", @@ -43,16 +42,20 @@ "@types/node": "^12.6.9", "codecov": "^3.5.0", "gts": "^1.1.0", - "mocha": "^6.2.0", + "mocha": "^6.2.2", "nyc": "^14.1.1", "rimraf": "^3.0.0", "ts-mocha": "^6.0.0", "ts-node": "^8.3.0", + "tslint-consistent-codestyle": "^1.16.0", "tslint-microsoft-contrib": "^6.2.0", "typescript": "3.7.2" }, "dependencies": { + "@opentelemetry/base": "^0.2.0", "@opentelemetry/core": "^0.2.0", - "@opentelemetry/types": "^0.2.0" + "@opentelemetry/metrics": "^0.2.0", + "@opentelemetry/types": "^0.2.0", + "prom-client": "^11.5.3" } } diff --git a/packages/opentelemetry-exporter-prometheus/src/export/types.ts b/packages/opentelemetry-exporter-prometheus/src/export/types.ts new file mode 100644 index 00000000000..69ac40cb132 --- /dev/null +++ b/packages/opentelemetry-exporter-prometheus/src/export/types.ts @@ -0,0 +1,53 @@ +/*! + * 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 types from '@opentelemetry/types'; + +/** + * Configuration interface for prometheus exporter + */ +export interface ExporterConfig { + /** + * App prefix for metrics, if needed + * + * @default '' + * */ + prefix?: string; + + /** + * Endpoint the metrics should be exposed at with preceeding slash + * @default '/metrics' + */ + endpoint?: string; + + /** + * Port number for Prometheus exporter server + * + * Default registered port is 9464: + * https://github.com/prometheus/prometheus/wiki/Default-port-allocations + * @default 9464 + */ + port?: number; + + /** + * Define if the Prometheus exporter server will be started + * @default false + */ + startServer?: boolean; + + /** Standard logging interface */ + logger?: types.Logger; +} diff --git a/packages/opentelemetry-exporter-prometheus/src/index.ts b/packages/opentelemetry-exporter-prometheus/src/index.ts index ab4fd7cc334..ff3d57cb444 100644 --- a/packages/opentelemetry-exporter-prometheus/src/index.ts +++ b/packages/opentelemetry-exporter-prometheus/src/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ -// +export * from './prometheus'; +export * from './export/types'; diff --git a/packages/opentelemetry-exporter-prometheus/src/prometheus.ts b/packages/opentelemetry-exporter-prometheus/src/prometheus.ts new file mode 100644 index 00000000000..69f45459098 --- /dev/null +++ b/packages/opentelemetry-exporter-prometheus/src/prometheus.ts @@ -0,0 +1,309 @@ +/*! + * 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 { + LabelValue, + MetricDescriptor, + MetricDescriptorType, + MetricExporter, + ReadableMetric, +} from '@opentelemetry/metrics'; +import * as types from '@opentelemetry/types'; +import { createServer, IncomingMessage, Server, ServerResponse } from 'http'; +import { Counter, Gauge, labelValues, Metric, Registry } from 'prom-client'; +import * as url from 'url'; +import { ExporterConfig } from './export/types'; + +export class PrometheusExporter implements MetricExporter { + static readonly DEFAULT_OPTIONS = { + port: 9464, + startServer: false, + endpoint: '/metrics', + prefix: '', + }; + + private readonly _registry = new Registry(); + private readonly _logger: types.Logger; + private readonly _port: number; + private readonly _endpoint: string; + private readonly _server: Server; + private readonly _prefix?: string; + private readonly _invalidCharacterRegex = /[^a-z0-9_]/gi; + + // This will be required when histogram is implemented. Leaving here so it is not forgotten + // Histogram cannot have a label named 'le' + // private static readonly RESERVED_HISTOGRAM_LABEL = 'le'; + + /** + * Constructor + * @param config Exporter configuration + * @param callback Callback to be called after a server was started + */ + constructor(config: ExporterConfig = {}, callback?: () => void) { + this._logger = config.logger || new NoopLogger(); + this._port = config.port || PrometheusExporter.DEFAULT_OPTIONS.port; + this._prefix = config.prefix || PrometheusExporter.DEFAULT_OPTIONS.prefix; + this._server = createServer(this._requestHandler); + + this._endpoint = ( + config.endpoint || PrometheusExporter.DEFAULT_OPTIONS.endpoint + ).replace(/^([^/])/, '/$1'); + + if (config.startServer || PrometheusExporter.DEFAULT_OPTIONS.startServer) { + this.startServer(callback); + } else if (callback) { + callback(); + } + } + + /** + * Saves the current values of all exported {@link ReadableMetric}s so that they can be pulled + * by the Prometheus backend. + * + * @todo reach into metrics to pull metric values on endpoint + * In its current state, the exporter saves the current values of all metrics when export + * is called and returns them when the export endpoint is called. In the future, this should + * be a no-op and the exporter should reach into the metrics when the export endpoint is + * called. As there is currently no interface to do this, this is our only option. + * + * @param readableMetrics Metrics to be sent to the prometheus backend + * @param cb result callback to be called on finish + */ + export( + readableMetrics: ReadableMetric[], + cb: (result: ExportResult) => void + ) { + if (!this._server) { + // It is conceivable that the _server may not be started as it is an async startup + // However unlikely, if this happens the caller may retry the export + cb(ExportResult.FAILED_RETRYABLE); + return; + } + + this._logger.debug('Prometheus exporter export'); + + for (const readableMetric of readableMetrics) { + this._updateMetric(readableMetric); + } + + cb(ExportResult.SUCCESS); + } + + /** + * Shuts down the export server and clears the registry + * + * @param cb called when server is stopped + */ + shutdown(cb?: () => void) { + this._registry.clear(); + this.stopServer(cb); + } + + /** + * Updates the value of a single metric in the registry + * + * @param readableMetric Metric value to be saved + */ + private _updateMetric(readableMetric: ReadableMetric) { + const metric = this._registerMetric(readableMetric); + if (!metric) return; + + const labelKeys = readableMetric.descriptor.labelKeys; + + if (metric instanceof Counter) { + for (const ts of readableMetric.timeseries) { + // Prometheus counter saves internal state and increments by given value. + // ReadableMetric value is the current state, not the delta to be incremented by. + // Currently, _registerMetric creates a new counter every time the value changes, + // so the increment here behaves as a set value (increment from 0) + metric.inc( + this._getLabelValues(labelKeys, ts.labelValues), + ts.points[0].value as number + ); + } + } + + if (metric instanceof Gauge) { + for (const ts of readableMetric.timeseries) { + metric.set( + this._getLabelValues(labelKeys, ts.labelValues), + ts.points[0].value as number + ); + } + } + + // TODO: only counter and gauge are implemented in metrics so far + } + + private _getLabelValues(keys: string[], values: LabelValue[]) { + const labelValues: labelValues = {}; + for (let i = 0; i < keys.length; i++) { + if (values[i].value !== null) { + labelValues[keys[i]] = values[i].value!; + } + } + return labelValues; + } + + private _registerMetric(readableMetric: ReadableMetric): Metric | undefined { + const metricName = this._getPrometheusMetricName(readableMetric.descriptor); + const metric = this._registry.getSingleMetric(metricName); + + /** + * Prometheus library does aggregation, which means its inc method must be called with + * the value to be incremented by. It does not have a set method. As our ReadableMetric + * contains the current value, not the value to be incremented by, we destroy and + * recreate counters when they are updated. + * + * This works because counters are identified by their name and no other internal ID + * https://prometheus.io/docs/instrumenting/exposition_formats/ + */ + if (metric instanceof Counter) { + this._registry.removeSingleMetric(metricName); + } else if (metric) return metric; + + return this._newMetric(readableMetric, metricName); + } + + private _newMetric( + readableMetric: ReadableMetric, + name: string + ): Metric | undefined { + const metricObject = { + name, + // prom-client throws with empty description which is our default + help: readableMetric.descriptor.description || 'description missing', + labelNames: readableMetric.descriptor.labelKeys, + // list of registries to register the newly created metric + registers: [this._registry], + }; + + switch (readableMetric.descriptor.type) { + case MetricDescriptorType.COUNTER_DOUBLE: + case MetricDescriptorType.COUNTER_INT64: + return new Counter(metricObject); + case MetricDescriptorType.GAUGE_DOUBLE: + case MetricDescriptorType.GAUGE_INT64: + return new Gauge(metricObject); + default: + // Other metric types are currently unimplemented + return undefined; + } + } + + private _getPrometheusMetricName(descriptor: MetricDescriptor): string { + return this._sanitizePrometheusMetricName( + this._prefix ? `${this._prefix}_${descriptor.name}` : descriptor.name + ); + } + + /** + * Ensures metric names are valid Prometheus metric names by removing + * characters allowed by OpenTelemetry but disallowed by Prometheus. + * + * https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels + * + * 1. Names must match `[a-zA-Z_:][a-zA-Z0-9_:]*` + * + * 2. Colons are reserved for user defined recording rules. + * They should not be used by exporters or direct instrumentation. + * + * OpenTelemetry metric names are already validated in the Meter when they are created, + * and they match the format `[a-zA-Z][a-zA-Z0-9_.\-]*` which is very close to a valid + * prometheus metric name, so we only need to strip characters valid in OpenTelemetry + * but not valid in prometheus and replace them with '_'. + * + * @param name name to be sanitized + */ + private _sanitizePrometheusMetricName(name: string): string { + return name.replace(this._invalidCharacterRegex, '_'); // replace all invalid characters with '_' + } + + /** + * Stops the Prometheus export server + * @param callback A callback that will be executed once the server is stopped + */ + stopServer(callback?: () => void) { + if (!this._server) { + this._logger.debug( + `Prometheus stopServer() was called but server was never started.` + ); + if (callback) { + callback(); + } + } else { + this._server.close(() => { + this._logger.debug(`Prometheus exporter was stopped`); + if (callback) { + callback(); + } + }); + } + } + + /** + * Starts the Prometheus export server + * + * @param callback called once the server is ready + */ + startServer(callback?: () => void) { + this._server.listen(this._port, () => { + this._logger.debug( + `Prometheus exporter started on port ${this._port} at endpoint ${this._endpoint}` + ); + if (callback) { + callback(); + } + }); + } + + /** + * Request handler used by http library to respond to incoming requests + * for the current state of metrics by the Prometheus backend. + * + * @param request Incoming HTTP request to export server + * @param response HTTP response object used to respond to request + */ + private _requestHandler = ( + request: IncomingMessage, + response: ServerResponse + ) => { + if (url.parse(request.url!).pathname === this._endpoint) { + this._exportMetrics(response); + } else { + this._notFound(response); + } + }; + + /** + * Responds to incoming message with current state of all metrics. + */ + private _exportMetrics = (response: ServerResponse) => { + response.statusCode = 200; + response.setHeader('content-type', this._registry.contentType); + response.end(this._registry.metrics() || '# no registered metrics'); + }; + + /** + * Responds with 404 status code to all requests that do not match the configured endpoint. + */ + private _notFound = (response: ServerResponse) => { + response.statusCode = 404; + response.end(); + }; +} diff --git a/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts b/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts new file mode 100644 index 00000000000..984a5feb4e2 --- /dev/null +++ b/packages/opentelemetry-exporter-prometheus/test/prometheus.test.ts @@ -0,0 +1,464 @@ +/*! + * 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 { CounterMetric, GaugeMetric, Meter } from '@opentelemetry/metrics'; +import * as assert from 'assert'; +import * as http from 'http'; +import { PrometheusExporter } from '../src'; + +describe('PrometheusExporter', () => { + describe('constructor', () => { + it('should construct an exporter', () => { + const exporter = new PrometheusExporter(); + assert.ok(typeof exporter.startServer === 'function'); + assert.ok(typeof exporter.shutdown === 'function'); + }); + + it('should start the server if startServer is passed as an option', done => { + const port = PrometheusExporter.DEFAULT_OPTIONS.port; + const endpoint = PrometheusExporter.DEFAULT_OPTIONS.endpoint; + const exporter = new PrometheusExporter( + { + startServer: true, + }, + () => { + const url = `http://localhost:${port}${endpoint}`; + http.get(url, function(res: any) { + assert.equal(res.statusCode, 200); + exporter.shutdown(() => { + return done(); + }); + }); + } + ); + }); + + it('should not start the server by default', () => { + const exporter = new PrometheusExporter(); + assert.ok(exporter['_server']!.listening === false); + }); + }); + + describe('server', () => { + it('it should start on startServer() and call the callback', done => { + const exporter = new PrometheusExporter({ + port: 9722, + }); + exporter.startServer(() => { + exporter.shutdown(() => { + return done(); + }); + }); + }); + + it('it should listen on the default port and default endpoint', done => { + const port = PrometheusExporter.DEFAULT_OPTIONS.port; + const endpoint = PrometheusExporter.DEFAULT_OPTIONS.endpoint; + const exporter = new PrometheusExporter(); + + exporter.startServer(() => { + const url = `http://localhost:${port}${endpoint}`; + http.get(url, function(res: any) { + assert.equal(res.statusCode, 200); + exporter.shutdown(() => { + return done(); + }); + }); + }); + }); + + it('it should listen on a custom port and endpoint if provided', done => { + const port = 9991; + const endpoint = '/metric'; + + const exporter = new PrometheusExporter({ + port, + endpoint, + }); + + exporter.startServer(() => { + const url = `http://localhost:${port}${endpoint}`; + http.get(url, function(res: any) { + assert.equal(res.statusCode, 200); + exporter.shutdown(() => { + return done(); + }); + }); + }); + }); + + it('it should not require endpoints to start with a slash', done => { + const port = 9991; + const endpoint = 'metric'; + + const exporter = new PrometheusExporter({ + port, + endpoint, + }); + + exporter.startServer(() => { + const url = `http://localhost:${port}/metric`; + http.get(url, function(res: any) { + assert.equal(res.statusCode, 200); + exporter.shutdown(() => { + const exporter2 = new PrometheusExporter({ + port, + endpoint: `/${endpoint}`, + }); + + exporter2.startServer(() => { + const url = `http://localhost:${port}/metric`; + http.get(url, function(res: any) { + assert.equal(res.statusCode, 200); + exporter2.stopServer(() => { + return done(); + }); + }); + }); + }); + }); + }); + }); + + it('it should return a HTTP status 404 if the endpoint does not match', done => { + const port = 9912; + const endpoint = '/metrics'; + const exporter = new PrometheusExporter({ + port, + endpoint, + }); + exporter.startServer(() => { + const url = `http://localhost:${port}/invalid`; + + http.get(url, function(res: any) { + assert.equal(res.statusCode, 404); + exporter.shutdown(() => { + return done(); + }); + }); + }); + }); + + it('should call a provided callback regardless of if the server is running', done => { + const exporter = new PrometheusExporter(); + exporter.shutdown(() => { + return done(); + }); + }); + }); + + describe('export', () => { + let exporter: PrometheusExporter; + let meter: Meter; + + beforeEach(done => { + exporter = new PrometheusExporter(); + meter = new Meter(); + exporter.startServer(done); + }); + + afterEach(done => { + exporter.shutdown(done); + }); + + it('should export a count aggregation', done => { + const counter = meter.createCounter('counter', { + description: 'a test description', + labelKeys: ['key1'], + }) as CounterMetric; + + const handle = counter.getHandle(meter.labels({ key1: 'labelValue1' })); + handle.add(10); + exporter.export(meter.getMetrics(), () => { + // This is to test the special case where counters are destroyed + // and recreated in the exporter in order to get around prom-client's + // aggregation and use ours. + handle.add(10); + exporter.export(meter.getMetrics(), () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.strictEqual( + lines[0], + '# HELP counter a test description' + ); + + assert.deepEqual(lines, [ + '# HELP counter a test description', + '# TYPE counter counter', + 'counter{key1="labelValue1"} 20', + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); + }); + + it('should export a gauge aggregation', done => { + const gauge = meter.createGauge('gauge', { + description: 'a test description', + labelKeys: ['key1'], + }) as GaugeMetric; + + const handle = gauge.getHandle(meter.labels({ key1: 'labelValue1' })); + handle.set(10); + exporter.export([gauge.get()!], () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepEqual(lines, [ + '# HELP gauge a test description', + '# TYPE gauge gauge', + 'gauge{key1="labelValue1"} 10', + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); + + it('should export a multiple aggregations', done => { + const gauge = meter.createGauge('gauge', { + description: 'a test description', + labelKeys: ['gaugeKey1'], + }) as GaugeMetric; + + const counter = meter.createCounter('counter', { + description: 'a test description', + labelKeys: ['counterKey1'], + }) as CounterMetric; + + gauge.getHandle(meter.labels({ key1: 'labelValue1' })).set(10); + counter.getHandle(meter.labels({ key1: 'labelValue1' })).add(10); + exporter.export([gauge.get()!, counter.get()!], () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepEqual(lines, [ + '# HELP gauge a test description', + '# TYPE gauge gauge', + 'gauge{gaugeKey1="labelValue1"} 10', + '', + '# HELP counter a test description', + '# TYPE counter counter', + 'counter{counterKey1="labelValue1"} 10', + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); + + it('should export a comment if no metrics are registered', done => { + exporter.export([], () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepEqual(lines, ['# no registered metrics']); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); + + it('should add a description if missing', done => { + const gauge = meter.createGauge('gauge') as GaugeMetric; + + const handle = gauge.getHandle(meter.labels({ key1: 'labelValue1' })); + handle.set(10); + exporter.export([gauge.get()!], () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepEqual(lines, [ + '# HELP gauge description missing', + '# TYPE gauge gauge', + 'gauge 10', + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); + + it('should sanitize names', done => { + const gauge = meter.createGauge('gauge.bad-name') as GaugeMetric; + const handle = gauge.getHandle(meter.labels({ key1: 'labelValue1' })); + handle.set(10); + exporter.export([gauge.get()!], () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepEqual(lines, [ + '# HELP gauge_bad_name description missing', + '# TYPE gauge_bad_name gauge', + 'gauge_bad_name 10', + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); + }); + + describe('configuration', () => { + let meter: Meter; + let gauge: GaugeMetric; + let exporter: PrometheusExporter | undefined; + + beforeEach(() => { + meter = new Meter(); + gauge = meter.createGauge('gauge') as GaugeMetric; + gauge.getHandle(meter.labels({ key1: 'labelValue1' })).set(10); + }); + + afterEach(done => { + if (exporter) { + exporter.shutdown(done); + exporter = undefined; + } else { + done(); + } + }); + + it('should use a configured name prefix', done => { + exporter = new PrometheusExporter({ + prefix: 'test_prefix', + }); + + exporter.startServer(() => { + exporter!.export(meter.getMetrics(), () => { + http + .get('http://localhost:9464/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepEqual(lines, [ + '# HELP test_prefix_gauge description missing', + '# TYPE test_prefix_gauge gauge', + 'test_prefix_gauge 10', + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); + }); + + it('should use a configured port', done => { + exporter = new PrometheusExporter({ + port: 8080, + }); + + exporter.startServer(() => { + exporter!.export(meter.getMetrics(), () => { + http + .get('http://localhost:8080/metrics', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepEqual(lines, [ + '# HELP gauge description missing', + '# TYPE gauge gauge', + 'gauge 10', + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); + }); + + it('should use a configured endpoint', done => { + exporter = new PrometheusExporter({ + endpoint: '/test', + }); + + exporter.startServer(() => { + exporter!.export(meter.getMetrics(), () => { + http + .get('http://localhost:9464/test', res => { + res.on('data', chunk => { + const body = chunk.toString(); + const lines = body.split('\n'); + + assert.deepEqual(lines, [ + '# HELP gauge description missing', + '# TYPE gauge gauge', + 'gauge 10', + '', + ]); + + done(); + }); + }) + .on('error', errorHandler(done)); + }); + }); + }); + }); +}); + +function errorHandler(done: Mocha.Done): (err: Error) => void { + return () => { + assert.ok(false, 'error getting metrics'); + done(); + }; +} diff --git a/packages/opentelemetry-exporter-prometheus/tslint.json b/packages/opentelemetry-exporter-prometheus/tslint.json new file mode 100644 index 00000000000..0710b135d07 --- /dev/null +++ b/packages/opentelemetry-exporter-prometheus/tslint.json @@ -0,0 +1,4 @@ +{ + "rulesDirectory": ["node_modules/tslint-microsoft-contrib"], + "extends": ["../../tslint.base.js", "./node_modules/tslint-consistent-codestyle"] +} From 36496f63d0b4790415e28a4bec1b3f93dfe96823 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Tue, 12 Nov 2019 12:06:29 -0800 Subject: [PATCH 10/17] chore: fix codecov path (#520) --- .../opentelemetry-plugin-pg/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/package.json b/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/package.json index 1508e87784e..e432168127c 100644 --- a/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/package.json +++ b/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/package.json @@ -12,7 +12,7 @@ "test:local": "cross-env RUN_POSTGRES_TESTS_LOCAL=true yarn test", "tdd": "yarn test -- --watch-extensions ts --watch", "clean": "rimraf build/*", - "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../", + "codecov": "nyc report --reporter=json && codecov -f coverage/*.json -p ../../../", "check": "gts check", "precompile": "tsc --version", "compile": "tsc -p .", From 57925939f0b7d41354ae2937ee84c8fd174a74ab Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Tue, 12 Nov 2019 13:23:12 -0800 Subject: [PATCH 11/17] chore: update dependency gts and codecov (#517) --- package.json | 2 +- packages/opentelemetry-base/package.json | 6 +++--- packages/opentelemetry-core/package.json | 6 +++--- packages/opentelemetry-exporter-jaeger/package.json | 2 +- packages/opentelemetry-exporter-prometheus/package.json | 2 +- packages/opentelemetry-exporter-zipkin/package.json | 2 +- packages/opentelemetry-metrics/package.json | 2 +- packages/opentelemetry-node/package.json | 4 ++-- packages/opentelemetry-plugin-document-load/package.json | 4 ++-- packages/opentelemetry-plugin-grpc/package.json | 2 +- packages/opentelemetry-plugin-http2/package.json | 2 +- packages/opentelemetry-plugin-mongodb/package.json | 2 +- .../opentelemetry-plugin-pg-pool/package.json | 4 ++-- .../opentelemetry-plugin-pg/package.json | 4 ++-- packages/opentelemetry-plugin-redis/package.json | 2 +- packages/opentelemetry-scope-async-hooks/package.json | 4 ++-- packages/opentelemetry-scope-base/package.json | 4 ++-- packages/opentelemetry-scope-zone-peer-dep/package.json | 4 ++-- packages/opentelemetry-scope-zone/package.json | 4 ++-- packages/opentelemetry-shim-opentracing/package.json | 2 +- packages/opentelemetry-tracing/package.json | 4 ++-- packages/opentelemetry-types/package.json | 2 +- packages/opentelemetry-web/package.json | 4 ++-- 23 files changed, 37 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index fa81f068915..120e8a4688a 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@commitlint/config-conventional": "^8.2.0", "beautify-benchmark": "^0.2.4", "benchmark": "^2.1.4", - "gts": "^1.0.0", + "gts": "^1.1.0", "husky": "^3.0.9", "lerna": "^3.17.0", "lerna-changelog": "^0.8.2", diff --git a/packages/opentelemetry-base/package.json b/packages/opentelemetry-base/package.json index 42cf058b579..9ec79269e94 100644 --- a/packages/opentelemetry-base/package.json +++ b/packages/opentelemetry-base/package.json @@ -1,7 +1,7 @@ { "name": "@opentelemetry/base", "version": "0.2.0", - "description": "OpenTelemetry base", + "description": "OpenTelemetry base provides base code for the SDK packages", "main": "build/src/index.js", "types": "build/src/index.d.ts", "repository": "open-telemetry/opentelemetry-js", @@ -43,8 +43,8 @@ "devDependencies": { "@types/mocha": "^5.2.5", "@types/node": "^12.6.8", - "codecov": "^3.1.0", - "gts": "^1.0.0", + "codecov": "^3.6.1", + "gts": "^1.1.0", "mocha": "^6.1.0", "nyc": "^14.1.1", "rimraf": "^3.0.0", diff --git a/packages/opentelemetry-core/package.json b/packages/opentelemetry-core/package.json index 11a6a47364f..8b56e65d9c3 100644 --- a/packages/opentelemetry-core/package.json +++ b/packages/opentelemetry-core/package.json @@ -1,7 +1,7 @@ { "name": "@opentelemetry/core", "version": "0.2.0", - "description": "OpenTelemetry Core", + "description": "OpenTelemetry Core provides default and no-op implementations of the OpenTelemetry types for trace and metrics", "main": "build/src/index.js", "browser": { "./src/platform/index.ts": "./src/platform/browser/index.ts", @@ -54,8 +54,8 @@ "@types/semver": "^6.2.0", "@types/sinon": "^7.0.13", "@types/webpack-env": "1.13.9", - "codecov": "^3.1.0", - "gts": "^1.0.0", + "codecov": "^3.6.1", + "gts": "^1.1.0", "istanbul-instrumenter-loader": "^3.0.1", "karma": "^4.4.1", "karma-chrome-launcher": "^3.1.0", diff --git a/packages/opentelemetry-exporter-jaeger/package.json b/packages/opentelemetry-exporter-jaeger/package.json index 798368b9e41..0fe7d1f8c6f 100644 --- a/packages/opentelemetry-exporter-jaeger/package.json +++ b/packages/opentelemetry-exporter-jaeger/package.json @@ -40,7 +40,7 @@ "devDependencies": { "@types/mocha": "^5.2.7", "@types/node": "^12.6.9", - "codecov": "^3.5.0", + "codecov": "^3.6.1", "gts": "^1.1.0", "mocha": "^6.2.0", "nyc": "^14.1.1", diff --git a/packages/opentelemetry-exporter-prometheus/package.json b/packages/opentelemetry-exporter-prometheus/package.json index 4361f1bf519..ac3c74ae1a5 100644 --- a/packages/opentelemetry-exporter-prometheus/package.json +++ b/packages/opentelemetry-exporter-prometheus/package.json @@ -40,7 +40,7 @@ "devDependencies": { "@types/mocha": "^5.2.7", "@types/node": "^12.6.9", - "codecov": "^3.5.0", + "codecov": "^3.6.1", "gts": "^1.1.0", "mocha": "^6.2.2", "nyc": "^14.1.1", diff --git a/packages/opentelemetry-exporter-zipkin/package.json b/packages/opentelemetry-exporter-zipkin/package.json index 0e6c7f5c55c..8a79cf9dfec 100644 --- a/packages/opentelemetry-exporter-zipkin/package.json +++ b/packages/opentelemetry-exporter-zipkin/package.json @@ -41,7 +41,7 @@ "@types/mocha": "^5.2.7", "@types/nock": "^10.0.3", "@types/node": "^12.6.9", - "codecov": "^3.5.0", + "codecov": "^3.6.1", "gts": "^1.1.0", "mocha": "^6.2.0", "nock": "^11.0.0", diff --git a/packages/opentelemetry-metrics/package.json b/packages/opentelemetry-metrics/package.json index a7db5104296..1ce12779a99 100644 --- a/packages/opentelemetry-metrics/package.json +++ b/packages/opentelemetry-metrics/package.json @@ -42,7 +42,7 @@ "@types/mocha": "^5.2.7", "@types/node": "^12.6.9", "@types/sinon": "^7.0.13", - "codecov": "^3.5.0", + "codecov": "^3.6.1", "gts": "^1.1.0", "mocha": "^6.2.0", "nyc": "^14.1.1", diff --git a/packages/opentelemetry-node/package.json b/packages/opentelemetry-node/package.json index 48a5910427c..46c8d593c02 100644 --- a/packages/opentelemetry-node/package.json +++ b/packages/opentelemetry-node/package.json @@ -44,8 +44,8 @@ "@types/node": "^12.6.8", "@types/semver": "^6.0.1", "@types/shimmer": "^1.0.1", - "codecov": "^3.1.0", - "gts": "^1.0.0", + "codecov": "^3.6.1", + "gts": "^1.1.0", "mocha": "^6.1.0", "nyc": "^14.1.1", "rimraf": "^3.0.0", diff --git a/packages/opentelemetry-plugin-document-load/package.json b/packages/opentelemetry-plugin-document-load/package.json index a7f615440a8..f002a2bb013 100644 --- a/packages/opentelemetry-plugin-document-load/package.json +++ b/packages/opentelemetry-plugin-document-load/package.json @@ -47,8 +47,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", + "codecov": "^3.6.1", + "gts": "^1.1.0", "istanbul-instrumenter-loader": "^3.0.1", "karma": "^4.4.1", "karma-chrome-launcher": "^3.1.0", diff --git a/packages/opentelemetry-plugin-grpc/package.json b/packages/opentelemetry-plugin-grpc/package.json index 9e14f27d0b5..a2f63e7348d 100644 --- a/packages/opentelemetry-plugin-grpc/package.json +++ b/packages/opentelemetry-plugin-grpc/package.json @@ -47,7 +47,7 @@ "@types/semver": "^6.2.0", "@types/shimmer": "^1.0.1", "@types/sinon": "^7.0.13", - "codecov": "^3.5.0", + "codecov": "^3.6.1", "grpc": "^1.23.3", "gts": "^1.1.0", "mocha": "^6.2.0", diff --git a/packages/opentelemetry-plugin-http2/package.json b/packages/opentelemetry-plugin-http2/package.json index 861e9b6d57b..063de1bcac5 100644 --- a/packages/opentelemetry-plugin-http2/package.json +++ b/packages/opentelemetry-plugin-http2/package.json @@ -42,7 +42,7 @@ "devDependencies": { "@types/mocha": "^5.2.7", "@types/node": "^12.6.9", - "codecov": "^3.5.0", + "codecov": "^3.6.1", "gts": "^1.1.0", "mocha": "^6.2.0", "nyc": "^14.1.1", diff --git a/packages/opentelemetry-plugin-mongodb/package.json b/packages/opentelemetry-plugin-mongodb/package.json index 1a78b4224e6..6c8b02219d7 100644 --- a/packages/opentelemetry-plugin-mongodb/package.json +++ b/packages/opentelemetry-plugin-mongodb/package.json @@ -42,7 +42,7 @@ "devDependencies": { "@types/mocha": "^5.2.7", "@types/node": "^12.6.9", - "codecov": "^3.5.0", + "codecov": "^3.6.1", "gts": "^1.1.0", "mocha": "^6.2.0", "nyc": "^14.1.1", diff --git a/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg-pool/package.json b/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg-pool/package.json index da7fbcca471..fc7aa348920 100644 --- a/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg-pool/package.json +++ b/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg-pool/package.json @@ -52,8 +52,8 @@ "@types/pg": "^7.11.2", "@types/shimmer": "^1.0.1", "@types/pg-pool": "^2.0.1", - "codecov": "^3.5.0", - "gts": "^1.0.0", + "codecov": "^3.6.1", + "gts": "^1.1.0", "mocha": "^6.2.0", "nyc": "^14.1.1", "rimraf": "^3.0.0", diff --git a/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/package.json b/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/package.json index e432168127c..09aefd38185 100644 --- a/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/package.json +++ b/packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/package.json @@ -49,8 +49,8 @@ "@types/node": "^12.6.9", "@types/pg": "^7.11.2", "@types/shimmer": "^1.0.1", - "codecov": "^3.5.0", - "gts": "^1.0.0", + "codecov": "^3.6.1", + "gts": "^1.1.0", "mocha": "^6.2.0", "nyc": "^14.1.1", "pg": "^7.12.1", diff --git a/packages/opentelemetry-plugin-redis/package.json b/packages/opentelemetry-plugin-redis/package.json index 93af0e8e55c..baa86c0f4f5 100644 --- a/packages/opentelemetry-plugin-redis/package.json +++ b/packages/opentelemetry-plugin-redis/package.json @@ -42,7 +42,7 @@ "devDependencies": { "@types/mocha": "^5.2.7", "@types/node": "^12.6.9", - "codecov": "^3.5.0", + "codecov": "^3.6.1", "gts": "^1.1.0", "mocha": "^6.2.0", "nyc": "^14.1.1", diff --git a/packages/opentelemetry-scope-async-hooks/package.json b/packages/opentelemetry-scope-async-hooks/package.json index fd1a5f562cd..452ffb10f9c 100644 --- a/packages/opentelemetry-scope-async-hooks/package.json +++ b/packages/opentelemetry-scope-async-hooks/package.json @@ -43,8 +43,8 @@ "@types/mocha": "^5.2.5", "@types/node": "^12.6.8", "@types/shimmer": "^1.0.1", - "codecov": "^3.1.0", - "gts": "^1.0.0", + "codecov": "^3.6.1", + "gts": "^1.1.0", "mocha": "^6.1.0", "nyc": "^14.1.1", "rimraf": "^3.0.0", diff --git a/packages/opentelemetry-scope-base/package.json b/packages/opentelemetry-scope-base/package.json index 635d10da61f..ef2733fffa3 100644 --- a/packages/opentelemetry-scope-base/package.json +++ b/packages/opentelemetry-scope-base/package.json @@ -43,8 +43,8 @@ "devDependencies": { "@types/mocha": "^5.2.5", "@types/node": "^12.6.8", - "codecov": "^3.1.0", - "gts": "^1.0.0", + "codecov": "^3.6.1", + "gts": "^1.1.0", "mocha": "^6.1.0", "nyc": "^14.1.1", "rimraf": "^3.0.0", diff --git a/packages/opentelemetry-scope-zone-peer-dep/package.json b/packages/opentelemetry-scope-zone-peer-dep/package.json index 0497800f462..81a3fbb9514 100644 --- a/packages/opentelemetry-scope-zone-peer-dep/package.json +++ b/packages/opentelemetry-scope-zone-peer-dep/package.json @@ -47,8 +47,8 @@ "@types/webpack-env": "1.13.9", "@types/zone.js": "^0.5.12", "babel-loader": "^8.0.6", - "codecov": "^3.1.0", - "gts": "^1.0.0", + "codecov": "^3.6.1", + "gts": "^1.1.0", "istanbul-instrumenter-loader": "^3.0.1", "karma": "^4.4.1", "karma-chrome-launcher": "^3.1.0", diff --git a/packages/opentelemetry-scope-zone/package.json b/packages/opentelemetry-scope-zone/package.json index 537571c6a45..c9d2976506e 100644 --- a/packages/opentelemetry-scope-zone/package.json +++ b/packages/opentelemetry-scope-zone/package.json @@ -43,8 +43,8 @@ "@types/sinon": "^7.0.13", "@babel/core": "^7.6.0", "babel-loader": "^8.0.6", - "codecov": "^3.1.0", - "gts": "^1.0.0", + "codecov": "^3.6.1", + "gts": "^1.1.0", "karma": "^4.1.0", "karma-chrome-launcher": "^2.2.0", "karma-mocha": "^1.3.0", diff --git a/packages/opentelemetry-shim-opentracing/package.json b/packages/opentelemetry-shim-opentracing/package.json index 482dbbe3d3b..5d2af4213fc 100644 --- a/packages/opentelemetry-shim-opentracing/package.json +++ b/packages/opentelemetry-shim-opentracing/package.json @@ -40,7 +40,7 @@ "@opentelemetry/tracing": "^0.2.0", "@types/mocha": "^5.2.7", "@types/node": "^12.6.9", - "codecov": "^3.5.0", + "codecov": "^3.6.1", "gts": "^1.1.0", "mocha": "^6.2.0", "nyc": "^14.1.1", diff --git a/packages/opentelemetry-tracing/package.json b/packages/opentelemetry-tracing/package.json index ee8e205ddcb..6b0a1f3d540 100644 --- a/packages/opentelemetry-tracing/package.json +++ b/packages/opentelemetry-tracing/package.json @@ -50,8 +50,8 @@ "@types/node": "^12.6.8", "@types/sinon": "^7.0.13", "@types/webpack-env": "1.13.9", - "codecov": "^3.1.0", - "gts": "^1.0.0", + "codecov": "^3.6.1", + "gts": "^1.1.0", "istanbul-instrumenter-loader": "^3.0.1", "karma": "^4.4.1", "karma-chrome-launcher": "^3.1.0", diff --git a/packages/opentelemetry-types/package.json b/packages/opentelemetry-types/package.json index 58288c09c4b..4c5345abb54 100644 --- a/packages/opentelemetry-types/package.json +++ b/packages/opentelemetry-types/package.json @@ -41,7 +41,7 @@ "access": "public" }, "devDependencies": { - "gts": "^1.0.0", + "gts": "^1.1.0", "linkinator": "^1.5.0", "tslint-consistent-codestyle": "^1.15.1", "tslint-microsoft-contrib": "^6.2.0", diff --git a/packages/opentelemetry-web/package.json b/packages/opentelemetry-web/package.json index 188f0df5b37..08e9b3c99e9 100644 --- a/packages/opentelemetry-web/package.json +++ b/packages/opentelemetry-web/package.json @@ -47,8 +47,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", + "codecov": "^3.6.1", + "gts": "^1.1.0", "istanbul-instrumenter-loader": "^3.0.1", "karma": "^4.4.1", "karma-chrome-launcher": "^3.1.0", From 668c3aa833707034569f3eb46f20fa2a9c0c0170 Mon Sep 17 00:00:00 2001 From: Xiao Date: Tue, 12 Nov 2019 13:40:54 -0800 Subject: [PATCH 12/17] feat: direct calling of metric instruments (#507) * feat: direct calling of metric instruments * fix: add interface in types->metric * fix: linting * fix: linting & docs * fix: add overloads for record in measure metric * fix: linting * fix: linting * fix: linting --- .../src/metrics/NoopMeter.ts | 43 ++++++++++++++++--- .../opentelemetry-metrics/src/LabelSet.ts | 9 ---- packages/opentelemetry-metrics/src/Metric.ts | 24 ++++++++++- .../opentelemetry-metrics/test/Meter.test.ts | 16 +++++++ .../opentelemetry-types/src/metrics/Metric.ts | 33 ++++++++++++++ 5 files changed, 107 insertions(+), 18 deletions(-) diff --git a/packages/opentelemetry-core/src/metrics/NoopMeter.ts b/packages/opentelemetry-core/src/metrics/NoopMeter.ts index 9f354cbaa07..6f2cb99507e 100644 --- a/packages/opentelemetry-core/src/metrics/NoopMeter.ts +++ b/packages/opentelemetry-core/src/metrics/NoopMeter.ts @@ -21,6 +21,7 @@ import { Meter, Metric, MetricOptions, + MetricUtils, MeasureHandle, SpanContext, LabelSet, @@ -110,6 +111,38 @@ export class NoopMetric implements Metric { } } +export class NoopCounterMetric extends NoopMetric + implements Pick { + add(value: number, labelSet: LabelSet) { + this.getHandle(labelSet).add(value); + } +} + +export class NoopGaugeMetric extends NoopMetric + implements Pick { + set(value: number, labelSet: LabelSet) { + this.getHandle(labelSet).set(value); + } +} + +export class NoopMeasureMetric extends NoopMetric + implements Pick { + record( + value: number, + labelSet: LabelSet, + distContext?: DistributedContext, + spanContext?: SpanContext + ) { + if (typeof distContext === 'undefined') { + this.getHandle(labelSet).record(value); + } else if (typeof spanContext === 'undefined') { + this.getHandle(labelSet).record(value, distContext); + } else { + this.getHandle(labelSet).record(value, distContext, spanContext); + } + } +} + export class NoopCounterHandle implements CounterHandle { add(value: number): void { return; @@ -133,16 +166,12 @@ export class NoopMeasureHandle implements MeasureHandle { } export const NOOP_GAUGE_HANDLE = new NoopGaugeHandle(); -export const NOOP_GAUGE_METRIC = new NoopMetric(NOOP_GAUGE_HANDLE); +export const NOOP_GAUGE_METRIC = new NoopGaugeMetric(NOOP_GAUGE_HANDLE); export const NOOP_COUNTER_HANDLE = new NoopCounterHandle(); -export const NOOP_COUNTER_METRIC = new NoopMetric( - NOOP_COUNTER_HANDLE -); +export const NOOP_COUNTER_METRIC = new NoopCounterMetric(NOOP_COUNTER_HANDLE); export const NOOP_MEASURE_HANDLE = new NoopMeasureHandle(); -export const NOOP_MEASURE_METRIC = new NoopMetric( - NOOP_MEASURE_HANDLE -); +export const NOOP_MEASURE_METRIC = new NoopMeasureMetric(NOOP_MEASURE_HANDLE); export const NOOP_LABEL_SET = {} as LabelSet; diff --git a/packages/opentelemetry-metrics/src/LabelSet.ts b/packages/opentelemetry-metrics/src/LabelSet.ts index c17534c2e78..fb038384bf0 100644 --- a/packages/opentelemetry-metrics/src/LabelSet.ts +++ b/packages/opentelemetry-metrics/src/LabelSet.ts @@ -28,12 +28,3 @@ export class LabelSet implements types.LabelSet { this.labels = labels; } } - -/** - * Type guard to remove nulls from arrays - * - * @param value value to be checked for null equality - */ -export function notNull(value: T | null): value is T { - return value !== null; -} diff --git a/packages/opentelemetry-metrics/src/Metric.ts b/packages/opentelemetry-metrics/src/Metric.ts index f3b50f2884a..e6bc268a80d 100644 --- a/packages/opentelemetry-metrics/src/Metric.ts +++ b/packages/opentelemetry-metrics/src/Metric.ts @@ -121,7 +121,8 @@ export abstract class Metric implements types.Metric { } /** This is a SDK implementation of Counter Metric. */ -export class CounterMetric extends Metric { +export class CounterMetric extends Metric + implements Pick { constructor( name: string, options: MetricOptions, @@ -145,10 +146,20 @@ export class CounterMetric extends Metric { this._onUpdate ); } + + /** + * Adds the given value to the current value. Values cannot be negative. + * @param value the value to add. + * @param labelSet the canonicalized LabelSet used to associate with this metric's handle. + */ + add(value: number, labelSet: types.LabelSet) { + this.getHandle(labelSet).add(value); + } } /** This is a SDK implementation of Gauge Metric. */ -export class GaugeMetric extends Metric { +export class GaugeMetric extends Metric + implements Pick { constructor( name: string, options: MetricOptions, @@ -172,4 +183,13 @@ export class GaugeMetric extends Metric { this._onUpdate ); } + + /** + * Sets the given value. Values can be negative. + * @param value the new value. + * @param labelSet the canonicalized LabelSet used to associate with this metric's handle. + */ + set(value: number, labelSet: types.LabelSet) { + this.getHandle(labelSet).set(value); + } } diff --git a/packages/opentelemetry-metrics/test/Meter.test.ts b/packages/opentelemetry-metrics/test/Meter.test.ts index 06fd599abca..3e678d7ce4c 100644 --- a/packages/opentelemetry-metrics/test/Meter.test.ts +++ b/packages/opentelemetry-metrics/test/Meter.test.ts @@ -76,6 +76,14 @@ describe('Meter', () => { assert.ok(counter instanceof Metric); }); + it('should be able to call add() directly on counter', () => { + const counter = meter.createCounter('name') as CounterMetric; + counter.add(10, labelSet); + assert.strictEqual(counter.getHandle(labelSet)['_data'], 10); + counter.add(10, labelSet); + assert.strictEqual(counter.getHandle(labelSet)['_data'], 20); + }); + describe('.getHandle()', () => { it('should create a counter handle', () => { const counter = meter.createCounter('name') as CounterMetric; @@ -229,6 +237,14 @@ describe('Meter', () => { assert.ok(gauge instanceof Metric); }); + it('should be able to call set() directly on gauge', () => { + const gauge = meter.createGauge('name') as GaugeMetric; + gauge.set(10, labelSet); + assert.strictEqual(gauge.getHandle(labelSet)['_data'], 10); + gauge.set(250, labelSet); + assert.strictEqual(gauge.getHandle(labelSet)['_data'], 250); + }); + describe('.getHandle()', () => { it('should create a gauge handle', () => { const gauge = meter.createGauge('name') as GaugeMetric; diff --git a/packages/opentelemetry-types/src/metrics/Metric.ts b/packages/opentelemetry-types/src/metrics/Metric.ts index 0c011d3811c..a1e18e4fadc 100644 --- a/packages/opentelemetry-types/src/metrics/Metric.ts +++ b/packages/opentelemetry-types/src/metrics/Metric.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +import { DistributedContext } from '../distributed_context/DistributedContext'; +import { SpanContext } from '../trace/span_context'; + /** * Options needed for metric creation */ @@ -99,6 +102,36 @@ export interface Metric { setCallback(fn: () => void): void; } +export interface MetricUtils { + /** + * Adds the given value to the current value. Values cannot be negative. + */ + add(value: number, labelSet: LabelSet): void; + + /** + * Sets the given value. Values can be negative. + */ + set(value: number, labelSet: LabelSet): void; + + /** + * Records the given value to this measure. + */ + record(value: number, labelSet: LabelSet): void; + + record( + value: number, + labelSet: LabelSet, + distContext: DistributedContext + ): void; + + record( + value: number, + labelSet: LabelSet, + distContext: DistributedContext, + spanContext: SpanContext + ): void; +} + /** * key-value pairs passed by the user. */ From f15dedbc97127046b8ececae36cea577173a3cd0 Mon Sep 17 00:00:00 2001 From: Daniel Dyla Date: Tue, 12 Nov 2019 18:56:07 -0500 Subject: [PATCH 13/17] ci: enumerate caching paths manually (#514) * ci: enumerate caching paths manually * chore: lint fix * ci: share cache with browser too * chore: remove yarn.lock from cache * chore: add prometheus deps to cache * fix: cache path * ci: remove errant cache restore --- .circleci/config.yml | 61 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 4134298bfa6..a52d62cdba1 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,6 +13,40 @@ postgres_service: &postgres_service POSTGRES_USER: postgres POSTGRES_DB: circle_database +cache_1: &cache_1 + key: npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-1 + paths: + - ./node_modules + - ./yarn.lock + - packages/opentelemetry-base/node_modules + - packages/opentelemetry-scope-base/node_modules + - packages/opentelemetry-types/node_modules + - packages/opentelemetry-scope-async-hooks/node_modules + - packages/opentelemetry-core/node_modules + - packages/opentelemetry-exporter-prometheus/node_modules + - packages/opentelemetry-metrics/node_modules + - packages/opentelemetry-tracing/node_modules + - packages/opentelemetry-exporter-jaeger/node_modules + - packages/opentelemetry-exporter-zipkin/node_modules + - packages/opentelemetry-node/node_modules + - packages/opentelemetry-shim-opentracing/node_modules + - packages/opentelemetry-web/node_modules + - packages/opentelemetry-plugin-dns/node_modules + +cache_2: &cache_2 + key: npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-2 + paths: + - packages/opentelemetry-plugin-grpc/node_modules + - packages/opentelemetry-plugin-http/node_modules + - packages/opentelemetry-plugin-http2/node_modules + - packages/opentelemetry-plugin-mongodb/node_modules + - packages/opentelemetry-plugin-redis/node_modules + - packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg/node_modules + - packages/opentelemetry-plugin-document-load/node_modules + - packages/opentelemetry-plugin-https/node_modules + - packages/opentelemetry-plugin-postgres/opentelemetry-plugin-pg-pool/node_modules + - packages/opentelemetry-exporter-prometheus/node_modules + node_unit_tests: &node_unit_tests steps: - checkout @@ -31,17 +65,17 @@ node_unit_tests: &node_unit_tests echo "CIRCLE_NODE_VERSION=${CIRCLE_NODE_VERSION}" - restore_cache: keys: - - npm-cache-{{ checksum "/tmp/checksums.txt" }} + - npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-1 + - restore_cache: + keys: + - npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-2 - run: name: Install Dependencies command: yarn install - save_cache: - key: npm-cache-{{ checksum "/tmp/checksums.txt" }} - paths: - - ./node_modules - - ./yarn.lock - - ./packages/*/node_modules - - ./packages/*/yarn.lock + <<: *cache_1 + - save_cache: + <<: *cache_2 - run: name: Compile code command: yarn compile @@ -55,6 +89,9 @@ node_unit_tests: &node_unit_tests browsers_unit_tests: &browsers_unit_tests steps: - checkout + - run: + name: Create Checksum + command: sh .circleci/checksum.sh /tmp/checksums.txt - run: name: Setup environment variables command: | @@ -65,9 +102,19 @@ browsers_unit_tests: &browsers_unit_tests command: | node --version echo "CIRCLE_NODE_VERSION=${CIRCLE_NODE_VERSION}" + - restore_cache: + keys: + - npm-cache-01-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-1 + - restore_cache: + keys: + - npm-cache-02-{{ .Environment.CIRCLE_JOB }}-{{ checksum "/tmp/checksums.txt" }}-1 - run: name: Install Dependencies command: yarn install + - save_cache: + <<: *cache_1 + - save_cache: + <<: *cache_2 - run: name: Compile code command: yarn compile From 8a81929d3b57e9d047e72411489a2441d0aa32e0 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Wed, 13 Nov 2019 09:54:36 -0800 Subject: [PATCH 14/17] chore(circleci): remove duplicate compile step (#510) --- .circleci/config.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index a52d62cdba1..2e3d019f2e8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -115,9 +115,6 @@ browsers_unit_tests: &browsers_unit_tests <<: *cache_1 - save_cache: <<: *cache_2 - - run: - name: Compile code - command: yarn compile - run: name: Unit tests command: yarn test:browser From 6f56c370f8dff60eea8adde6e4c355d98e3af4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerhard=20St=C3=B6bich?= <18708370+Flarna@users.noreply.github.com> Date: Wed, 13 Nov 2019 23:45:42 +0100 Subject: [PATCH 15/17] chore: add .vscode to gitignore (#528) --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 8f5f17ecffa..f9d00899257 100644 --- a/.gitignore +++ b/.gitignore @@ -74,3 +74,6 @@ docs # OS generated files .DS_Store + +# VsCode configs +.vscode/ From 631d9e4f59a6484460f9f429007a6c98040253e8 Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Thu, 14 Nov 2019 11:19:48 -0800 Subject: [PATCH 16/17] chore: update prometheus exporter readme with usage and links (#521) * chore: update prometheus exporter readme with example and links * fix: minor change --- .../README.md | 53 ++++++++++++++++++- 1 file changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/opentelemetry-exporter-prometheus/README.md b/packages/opentelemetry-exporter-prometheus/README.md index ef4b68d30f3..4ac8bc579bb 100644 --- a/packages/opentelemetry-exporter-prometheus/README.md +++ b/packages/opentelemetry-exporter-prometheus/README.md @@ -4,9 +4,58 @@ [![devDependencies][devDependencies-image]][devDependencies-url] [![Apache License][license-image]][license-image] -OpenTelemetry Exporter Prometheus provides a metrics endpoint for Prometheus. +The OpenTelemetry Prometheus Metrics Exporter allows the user to send collected [OpenTelemetry Metrics](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-metrics) to Prometheus. -**Work in progress** +[Prometheus](https://prometheus.io/) is a monitoring system that collects metrics, by scraping exposed endpoints at regular intervals, evaluating rule expressions. It can also trigger alerts if certain conditions are met. For assistance setting up Prometheus, [Click here](https://opencensus.io/codelabs/prometheus/#0) for a guided codelab. + +## Installation + +```bash +npm install --save @opentelemetry/metrics +npm install --save @opentelemetry/exporter-prometheus +``` + +## Usage + +Create & register the exporter on your application. + +```js +const { PrometheusExporter } = require('@opentelemetry/exporter-prometheus'); +const { Meter } = require('@opentelemetry/metrics'); + +// Add your port and startServer to the Prometheus options +const options = {port: 9464, startServer: true}; +const exporter = new PrometheusExporter(options); + +// Register the exporter +const meter = new Meter(); +meter.addExporter(exporter); + +// Now, start recording data +const counter = meter.createCounter('metric_name'); +counter.add(10, meter.labels({ [key]: 'value' })); + +// Record data using Handle: It is recommended to keep a reference to the Handle instead of +// always calling `getHandle()` method for every operations. +const handle = counter.getHandle(meter.labels({ [key]: 'value' })); +handle.add(10); + +// .. some other work + +// Create and record Gauge +const gauge = meter.createGauge('metric_name1'); +gauge.set(10, meter.labels({ [key1]: 'value1' })); +``` + +## Viewing your metrics + +With the above you should now be able to navigate to the Prometheus UI at: http://localhost:9464/metrics + +## Useful links +- For more information on OpenTelemetry, visit: +- To learn more about Prometheus, visit: https://prometheus.io/ +- For more about OpenTelemetry JavaScript: +- For help or feedback on this project, join us on [gitter][gitter-url] ## License From 0c1ff8dec9aad4cb69232d85c6322cb78d5ca6dd Mon Sep 17 00:00:00 2001 From: Mayur Kale Date: Fri, 15 Nov 2019 07:37:03 -0800 Subject: [PATCH 17/17] chore: update README with plugins work and add DNS plugin in default supported plugins list (#529) --- README.md | 5 ++++- packages/opentelemetry-node/src/config.ts | 4 ++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 95d5b443796..2a2a87238d1 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ Maintainers ([@open-telemetry/js-maintainers](https://github.com/orgs/open-telem | [@opentelemetry/metrics](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-metrics) | This module provides instruments and meters for reporting of time series data. | | [@opentelemetry/node](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-node) | This module provides automatic tracing for Node.js applications. It is intended for use on the server only. | | [@opentelemetry/web](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-web) | This module provides automated instrumentation and tracing for Web applications. It is intended for use in the browser only. | +| [@opentelemetry/base](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-base) | This package provides base code for the SDK packages (tracing and metrics). | ### Exporters @@ -99,7 +100,7 @@ OpenTelemetry is vendor-agnostic and can upload data to any backend with various - [@opentelemetry/exporter-zipkin](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-exporter-zipkin) #### Metric Exporters -- [@opentelemetry/exporter-prometheus](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-exporter-prometheus) - WIP +- [@opentelemetry/exporter-prometheus](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-exporter-prometheus) ### Plugins @@ -112,6 +113,8 @@ OpenTelemetry can collect tracing data automatically using plugins. Vendors/User - [@opentelemetry/plugin-dns](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-dns) - [@opentelemetry/plugin-mongodb](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-mongodb) - WIP - [@opentelemetry/plugin-postgres](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-postgres) - WIP +- [@opentelemetry/plugin-redis](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-redis) - WIP +- [@opentelemetry/plugin-mysql](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-mysql) - WIP #### Web Plugins - [@opentelemetry/plugin-document-load](https://github.com/open-telemetry/opentelemetry-js/tree/master/packages/opentelemetry-plugin-document-load) diff --git a/packages/opentelemetry-node/src/config.ts b/packages/opentelemetry-node/src/config.ts index 10fe664d424..7d0eb09175a 100644 --- a/packages/opentelemetry-node/src/config.ts +++ b/packages/opentelemetry-node/src/config.ts @@ -39,4 +39,8 @@ export const DEFAULT_INSTRUMENTATION_PLUGINS: Plugins = { enabled: true, path: '@opentelemetry/plugin-https', }, + dns: { + enabled: true, + path: '@opentelemetry/plugin-dns', + }, };