From 9d78d165275683896b08adef1b2e52bc212778e5 Mon Sep 17 00:00:00 2001 From: Maor Leger Date: Thu, 13 Jan 2022 15:22:50 -0800 Subject: [PATCH] [core] - Extract OpenTelemetry to a separate package (#19734) ## What - Rewrite core-tracing using an abstraction of an instrumenter - Remove @opentelemetry packages from core-tracing - Introduce a stateful client that can be used to interact with the underlying instrumenter - Introduce @azure/opentelemetry-instrumentation-azure-sdk package providing an OTel instrumenter - Integrate this package with OpenTelemetry's instrumentation APIs to provide a plug-in that lights up OTel based tracing - Add a `chaiAzureTrace` chai plugin to allow for quick and simple validation of tracing logic for packages ## Why ### Core-tracing This is essentially a rewrite of core-tracing to abstract away from @opentelemetry/api for multiple reasons: 1. Decreased bundle size 2. Making OTel tracing truly opt-in. If you don't use OTel, there's no reason for us to call OTel APIs. While the APIs is now stable, it has been problematic for us in the past. 3. No enums, no TS > 3.9 features to downlevel 3. Finally, @opentelemetry/api is very flexible and able to support many scenarios at the cost of what is a very wide API surface area. We can avoid much of this complexity. ### new instrumentation package With the changes to core-tracing, we now need a way to tie our abstraction to a concrete OTel implementation. A new package, `@azure/opentelemetry-instrumentation-azure-sdk` provides this hook using well-known `registerInstrumentations` API to light-up OTel based tracing when a user opts-in. ### New chai plugin This is our attempt at reducing the boilerplate around tracing, providing what is essentially a custom assertion wrapped in a chai plugin. While this may change significantly, I feel comfortable merging this in and making follow-up changes on main. --- common/config/rush/pnpm-lock.yaml | 523 +++++++++++++++- rush.json | 7 +- sdk/core/core-auth/review/core-auth.api.md | 15 +- sdk/core/core-auth/src/index.ts | 2 +- sdk/core/core-auth/src/tokenCredential.ts | 6 +- sdk/core/core-auth/src/tracing.ts | 6 +- sdk/core/core-tracing/CHANGELOG.md | 7 +- sdk/core/core-tracing/package.json | 6 +- .../core-tracing/review/core-tracing.api.md | 201 ++---- sdk/core/core-tracing/src/createSpan.ts | 215 ------- sdk/core/core-tracing/src/index.ts | 49 +- sdk/core/core-tracing/src/instrumenter.ts | 77 +++ sdk/core/core-tracing/src/interfaces.ts | 588 ++++++------------ sdk/core/core-tracing/src/tracingClient.ts | 121 ++++ sdk/core/core-tracing/src/tracingContext.ts | 74 +++ .../src/utils/traceParentHeader.ts | 63 -- sdk/core/core-tracing/test/createSpan.spec.ts | 269 -------- .../core-tracing/test/instrumenter.spec.ts | 90 +++ sdk/core/core-tracing/test/interfaces.spec.ts | 91 +-- .../test/traceParentHeader.spec.ts | 125 ---- .../core-tracing/test/tracingClient.spec.ts | 197 ++++++ .../core-tracing/test/tracingContext.spec.ts | 121 ++++ .../test/util/testTracerProvider.ts | 25 - sdk/instrumentation/ci.yml | 30 + .../.nycrc | 19 + .../CHANGELOG.md | 13 + .../LICENSE | 21 + .../README.md | 94 +++ .../api-extractor.json | 31 + .../karma.conf.js | 130 ++++ .../package.json | 135 ++++ ...telemetry-instrumentation-azure-sdk.api.md | 23 + .../rollup.config.js | 3 + .../src/constants.ts} | 3 +- .../src/index.ts | 5 + .../src/instrumentation.browser.ts | 64 ++ .../src/instrumentation.ts | 81 +++ .../src/instrumenter.ts | 65 ++ .../src/logger.ts | 9 + .../src/spanWrapper.ts | 54 ++ .../src/transformations.ts | 95 +++ .../test/public/instrumenter.spec.ts | 244 ++++++++ .../test/public/spanWrapper.spec.ts | 93 +++ .../test/public}/util/testSpan.ts | 44 +- .../test/public}/util/testTracer.ts | 26 +- .../test/public/util/testTracerProvider.ts | 41 ++ .../tests.yml | 11 + .../tsconfig.json | 11 + .../tsdoc.json | 4 + sdk/test-utils/test-utils/package.json | 4 +- sdk/test-utils/test-utils/src/index.ts | 6 +- .../test-utils/src/tracing/chaiAzureTrace.ts | 152 +++++ .../test-utils/src/tracing/mockContext.ts | 44 ++ .../src/tracing/mockInstrumenter.ts | 110 ++++ .../test-utils/src/tracing/mockTracingSpan.ts | 85 +++ .../test-utils/src/tracing/spanGraphModel.ts | 28 + .../test-utils/src/tracing/testSpan.ts | 19 +- .../test-utils/src/tracing/testTracer.ts | 67 +- .../src/tracing/testTracerProvider.ts | 30 +- .../test/tracing/mockInstrumenter.spec.ts | 130 ++++ .../test/tracing/mockTracingSpan.spec.ts | 37 ++ 61 files changed, 3483 insertions(+), 1456 deletions(-) delete mode 100644 sdk/core/core-tracing/src/createSpan.ts create mode 100644 sdk/core/core-tracing/src/instrumenter.ts create mode 100644 sdk/core/core-tracing/src/tracingClient.ts create mode 100644 sdk/core/core-tracing/src/tracingContext.ts delete mode 100644 sdk/core/core-tracing/src/utils/traceParentHeader.ts delete mode 100644 sdk/core/core-tracing/test/createSpan.spec.ts create mode 100644 sdk/core/core-tracing/test/instrumenter.spec.ts delete mode 100644 sdk/core/core-tracing/test/traceParentHeader.spec.ts create mode 100644 sdk/core/core-tracing/test/tracingClient.spec.ts create mode 100644 sdk/core/core-tracing/test/tracingContext.spec.ts delete mode 100644 sdk/core/core-tracing/test/util/testTracerProvider.ts create mode 100644 sdk/instrumentation/ci.yml create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/.nycrc create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/CHANGELOG.md create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/LICENSE create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/README.md create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/api-extractor.json create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/karma.conf.js create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/package.json create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/review/opentelemetry-instrumentation-azure-sdk.api.md create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/rollup.config.js rename sdk/{core/core-tracing/src/utils/browser.d.ts => instrumentation/opentelemetry-instrumentation-azure-sdk/src/constants.ts} (53%) create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/index.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.browser.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/logger.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/spanWrapper.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/transformations.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/instrumenter.spec.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/spanWrapper.spec.ts rename sdk/{core/core-tracing/test => instrumentation/opentelemetry-instrumentation-azure-sdk/test/public}/util/testSpan.ts (84%) rename sdk/{core/core-tracing/test => instrumentation/opentelemetry-instrumentation-azure-sdk/test/public}/util/testTracer.ts (88%) create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracerProvider.ts create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tests.yml create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsconfig.json create mode 100644 sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsdoc.json create mode 100644 sdk/test-utils/test-utils/src/tracing/chaiAzureTrace.ts create mode 100644 sdk/test-utils/test-utils/src/tracing/mockContext.ts create mode 100644 sdk/test-utils/test-utils/src/tracing/mockInstrumenter.ts create mode 100644 sdk/test-utils/test-utils/src/tracing/mockTracingSpan.ts create mode 100644 sdk/test-utils/test-utils/src/tracing/spanGraphModel.ts create mode 100644 sdk/test-utils/test-utils/test/tracing/mockInstrumenter.spec.ts create mode 100644 sdk/test-utils/test-utils/test/tracing/mockTracingSpan.spec.ts diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index d2174fe8ccf1..637761f3ab22 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -146,6 +146,7 @@ specifiers: '@rush-temp/mock-hub': file:./projects/mock-hub.tgz '@rush-temp/monitor-opentelemetry-exporter': file:./projects/monitor-opentelemetry-exporter.tgz '@rush-temp/monitor-query': file:./projects/monitor-query.tgz + '@rush-temp/opentelemetry-instrumentation-azure-sdk': file:./projects/opentelemetry-instrumentation-azure-sdk.tgz '@rush-temp/perf-ai-form-recognizer': file:./projects/perf-ai-form-recognizer.tgz '@rush-temp/perf-ai-metrics-advisor': file:./projects/perf-ai-metrics-advisor.tgz '@rush-temp/perf-ai-text-analytics': file:./projects/perf-ai-text-analytics.tgz @@ -344,6 +345,7 @@ dependencies: '@rush-temp/mock-hub': file:projects/mock-hub.tgz '@rush-temp/monitor-opentelemetry-exporter': file:projects/monitor-opentelemetry-exporter.tgz '@rush-temp/monitor-query': file:projects/monitor-query.tgz + '@rush-temp/opentelemetry-instrumentation-azure-sdk': file:projects/opentelemetry-instrumentation-azure-sdk.tgz '@rush-temp/perf-ai-form-recognizer': file:projects/perf-ai-form-recognizer.tgz '@rush-temp/perf-ai-metrics-advisor': file:projects/perf-ai-metrics-advisor.tgz '@rush-temp/perf-ai-text-analytics': file:projects/perf-ai-text-analytics.tgz @@ -1382,6 +1384,11 @@ packages: '@opentelemetry/api': 1.0.3 dev: false + /@opentelemetry/api-metrics/0.27.0: + resolution: {integrity: sha512-tB79288bwjkdhPNpw4UdOEy3bacVwtol6Que7cAu8KEJ9ULjRfSiwpYEwJY/oER3xZ7zNFz0uiJ7N1jSiotpVA==} + engines: {node: '>=8.0.0'} + dev: false + /@opentelemetry/api/0.10.2: resolution: {integrity: sha512-GtpMGd6vkzDMYcpu2t9LlhEgMy/SzBwRnz48EejlRArYqZzqSzAsKmegUK7zHgl+EOIaK9mKHhnRaQu3qw20cA==} engines: {node: '>=8.0.0'} @@ -1424,6 +1431,16 @@ packages: semver: 7.3.5 dev: false + /@opentelemetry/core/1.0.1_@opentelemetry+api@1.0.3: + resolution: {integrity: sha512-90nQ2X6b/8X+xjcLDBYKooAcOsIlwLRYm+1VsxcX5cHl6V4CSVmDpBreQSDH/A21SqROzapk6813008SatmPpQ==} + engines: {node: '>=8.5.0'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.1.0' + dependencies: + '@opentelemetry/api': 1.0.3 + '@opentelemetry/semantic-conventions': 1.0.1 + dev: false + /@opentelemetry/instrumentation-http/0.22.0_@opentelemetry+api@1.0.3: resolution: {integrity: sha512-vqM1hqgYtcO8Upq8pl4I+YW0bnodHlUSSKYuOH7m9Aujbi571pU3zFctpiU5pNhj9eLEJ/r7aOTV6O4hCxqOjQ==} engines: {node: '>=8.0.0'} @@ -1453,6 +1470,20 @@ packages: - supports-color dev: false + /@opentelemetry/instrumentation/0.27.0_@opentelemetry+api@1.0.3: + resolution: {integrity: sha512-dUwY/VoDptdK8AYigwS3IKblG+unV5xIdV4VQKy+nX5aT3f7vd5PMYs4arCQSYLbLRe0s7GxK6S9dtjai/TsHQ==} + peerDependencies: + '@opentelemetry/api': ^1.0.0 + dependencies: + '@opentelemetry/api': 1.0.3 + '@opentelemetry/api-metrics': 0.27.0 + require-in-the-middle: 5.1.0 + semver: 7.3.5 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + dev: false + /@opentelemetry/node/0.22.0_@opentelemetry+api@1.0.3: resolution: {integrity: sha512-+HhGbDruQ7cwejVOIYyxRa28uosnG8W95NiQZ6qE8PXXPsDSyGeftAPbtYpGit0H2f5hrVcMlwmWHeAo9xkSLA==} engines: {node: '>=8.0.0'} @@ -1509,6 +1540,11 @@ packages: engines: {node: '>=8.0.0'} dev: false + /@opentelemetry/semantic-conventions/1.0.1: + resolution: {integrity: sha512-7XU1sfQ8uCVcXLxtAHA8r3qaLJ2oq7sKtEwzZhzuEXqYmjW+n+J4yM3kNo0HQo3Xp1eUe47UM6Wy6yuAvIyllg==} + engines: {node: '>=8.0.0'} + dev: false + /@opentelemetry/tracing/0.22.0_@opentelemetry+api@1.0.3: resolution: {integrity: sha512-EFrKTFndiEdh/KnzwDgo/EcphG/5z/NyLck8oiUUY+YMP7hskXNYHjTWSAv9UxtYe1MzgLbjmAateTuMmFIVNw==} engines: {node: '>=8.0.0'} @@ -1646,6 +1682,18 @@ packages: '@sinonjs/commons': 1.8.3 dev: false + /@sinonjs/fake-timers/7.1.2: + resolution: {integrity: sha512-iQADsW4LBMISqZ6Ci1dupJL9pprqwcVFTcOsEmQOEhW+KLCVn/Y4Jrvg2k19fIHCp+iFprriYPTdRcQR8NbUPg==} + dependencies: + '@sinonjs/commons': 1.8.3 + dev: false + + /@sinonjs/fake-timers/8.1.0: + resolution: {integrity: sha512-OAPJUAtgeINhh/TAlUID4QTs53Njm7xzddaVlEs/SXwgtiD1tW22zAB/W1wdqfrpmikgaWQ9Fw6Ws+hsiRm5Vg==} + dependencies: + '@sinonjs/commons': 1.8.3 + dev: false + /@sinonjs/samsam/5.3.1: resolution: {integrity: sha512-1Hc0b1TtyfBu8ixF/tpfSHTVWKwCBLY4QJbkgnE7HcwyvT2xArDxb4K7dMgqRm3szI+LJbzmW/s4xxEhv6hwDg==} dependencies: @@ -1654,6 +1702,14 @@ packages: type-detect: 4.0.8 dev: false + /@sinonjs/samsam/6.0.2: + resolution: {integrity: sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==} + dependencies: + '@sinonjs/commons': 1.8.3 + lodash.get: 4.4.2 + type-detect: 4.0.8 + dev: false + /@sinonjs/text-encoding/0.7.1: resolution: {integrity: sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==} dev: false @@ -1830,6 +1886,10 @@ packages: resolution: {integrity: sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==} dev: false + /@types/minimatch/3.0.3: + resolution: {integrity: sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==} + dev: false + /@types/minimatch/3.0.5: resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==} dev: false @@ -1914,6 +1974,12 @@ packages: '@types/node': 17.0.1 dev: false + /@types/sinon/10.0.6: + resolution: {integrity: sha512-6EF+wzMWvBNeGrfP3Nx60hhx+FfwSg1JJBLAAP/IdIUq0EYkqCYf70VT3PhuhPX9eLD+Dp+lNdpb/ZeHG8Yezg==} + dependencies: + '@sinonjs/fake-timers': 7.1.2 + dev: false + /@types/sinon/9.0.11: resolution: {integrity: sha512-PwP4UY33SeeVKodNE37ZlOsR9cReypbMJOhZ7BVE0lB+Hix3efCOxiJWiE5Ia+yL9Cn2Ch72EjFTRze8RZsNtg==} dependencies: @@ -2218,6 +2284,13 @@ packages: picomatch: 2.3.0 dev: false + /append-transform/1.0.0: + resolution: {integrity: sha512-P009oYkeHyU742iSZJzZZywj4QRJdnTWffaKuJQLablCZ1uz6/cW4yaRgcDaoQ+uwOxxnt0gRUcwfsNP2ri0gw==} + engines: {node: '>=4'} + dependencies: + default-require-extensions: 2.0.0 + dev: false + /append-transform/2.0.0: resolution: {integrity: sha512-7yeyCEurROLQJFv5Xj4lEGTy0borxepjFv1g22oAdqFu//SrAlDl1O1Nxx15SH1RoliUml6p8dwJW9jvZughhg==} engines: {node: '>=8'} @@ -2416,6 +2489,12 @@ packages: regenerator-runtime: 0.11.1 dev: false + /backbone/1.4.0: + resolution: {integrity: sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ==} + dependencies: + underscore: 1.13.2 + dev: false + /balanced-match/1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: false @@ -2534,6 +2613,16 @@ packages: engines: {node: '>= 0.8'} dev: false + /caching-transform/3.0.2: + resolution: {integrity: sha512-Mtgcv3lh3U0zRii/6qVgQODdPA4G3zhG+jtbCWj39RXuUFTMzH0vcdMtaJS1jPowd+It2Pqr6y3NJMQqOqCE2w==} + engines: {node: '>=6'} + dependencies: + hasha: 3.0.0 + make-dir: 2.1.0 + package-hash: 3.0.0 + write-file-atomic: 2.4.3 + dev: false + /caching-transform/4.0.0: resolution: {integrity: sha512-kpqOvwXnjjN44D89K5ccQC+RUrsy7jB/XLlRrx0D7/2HNcTPqzsb6XgYoErwko6QsV184CA2YgS1fxDiiDZMWA==} engines: {node: '>=8'} @@ -2861,6 +2950,17 @@ packages: vary: 1.1.2 dev: false + /cp-file/6.2.0: + resolution: {integrity: sha512-fmvV4caBnofhPe8kOcitBwSn2f39QLjnAnGq3gO9dfd75mUytzKNZB1hde6QHunW2Rt+OwuBOMc3i1tNElbszA==} + engines: {node: '>=6'} + dependencies: + graceful-fs: 4.2.8 + make-dir: 2.1.0 + nested-error-stacks: 2.1.0 + pify: 4.0.1 + safe-buffer: 5.2.1 + dev: false + /create-require/1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: false @@ -2873,6 +2973,13 @@ packages: cross-spawn: 7.0.3 dev: false + /cross-spawn/4.0.2: + resolution: {integrity: sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE=} + dependencies: + lru-cache: 4.1.5 + which: 1.3.1 + dev: false + /cross-spawn/6.0.5: resolution: {integrity: sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==} engines: {node: '>=4.8'} @@ -3020,6 +3127,13 @@ packages: engines: {node: '>=0.10.0'} dev: false + /default-require-extensions/2.0.0: + resolution: {integrity: sha1-9fj7sYp9bVCyH2QfZJ67Uiz+JPc=} + engines: {node: '>=4'} + dependencies: + strip-bom: 3.0.0 + dev: false + /default-require-extensions/3.0.0: resolution: {integrity: sha512-ek6DpXq/SCpvjhpFsLFRVtIxJCRw6fUR42lYMVZuUMK7n8eMz4Uh5clckdBjEpLhn/gEBZo7hDJnJcwdKLKQjg==} engines: {node: '>=8'} @@ -3086,6 +3200,11 @@ packages: engines: {node: '>=0.3.1'} dev: false + /diff/5.0.0: + resolution: {integrity: sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==} + engines: {node: '>=0.3.1'} + dev: false + /dir-glob/3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -3134,6 +3253,14 @@ packages: engines: {node: '>=10'} dev: false + /downlevel-dts/0.4.0: + resolution: {integrity: sha512-nh5vM3n2pRhPwZqh0iWo5gpItPAYEGEWw9yd0YpI+lO60B7A3A6iJlxDbt7kKVNbqBXKsptL+jwE/Yg5Go66WQ==} + hasBin: true + dependencies: + shelljs: 0.8.4 + typescript: 3.9.10 + dev: false + /downlevel-dts/0.8.0: resolution: {integrity: sha512-wBy+Q0Ya/1XRz9MMaj3BXH95E8aSckY3lppmUnf8Qv7dUg0wbWm3szDiVL4PdAvwcS7JbBBDPhCXeAGNT3ttFQ==} hasBin: true @@ -3733,6 +3860,15 @@ packages: unpipe: 1.0.0 dev: false + /find-cache-dir/2.1.0: + resolution: {integrity: sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==} + engines: {node: '>=6'} + dependencies: + commondir: 1.0.1 + make-dir: 2.1.0 + pkg-dir: 3.0.0 + dev: false + /find-cache-dir/3.3.2: resolution: {integrity: sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==} engines: {node: '>=8'} @@ -3817,6 +3953,13 @@ packages: resolution: {integrity: sha1-C+4AUBiusmDQo6865ljdATbsG5k=} dev: false + /foreground-child/1.5.6: + resolution: {integrity: sha1-T9ca0t/elnibmApcCilZN8svXOk=} + dependencies: + cross-spawn: 4.0.2 + signal-exit: 3.0.6 + dev: false + /foreground-child/2.0.0: resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} engines: {node: '>=8.0.0'} @@ -4088,6 +4231,19 @@ packages: resolution: {integrity: sha512-Y8T4vYhEfwJOTbouREvG+3XDsjr8E3kIr7uf+JZ0BYloFsttiHU0WfvANVsR7TxNUJa/WpCnw/Ino/p+DeBhBQ==} dev: false + /handlebars/4.7.7: + resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} + engines: {node: '>=0.4.7'} + hasBin: true + dependencies: + minimist: 1.2.5 + neo-async: 2.6.2 + source-map: 0.6.1 + wordwrap: 1.0.0 + optionalDependencies: + uglify-js: 3.14.5 + dev: false + /has-ansi/2.0.0: resolution: {integrity: sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=} engines: {node: '>=0.10.0'} @@ -4144,6 +4300,13 @@ packages: function-bind: 1.1.1 dev: false + /hasha/3.0.0: + resolution: {integrity: sha1-UqMvq4Vp1BymmmH/GiFPjrfIvTk=} + engines: {node: '>=4'} + dependencies: + is-stream: 1.1.0 + dev: false + /hasha/5.2.2: resolution: {integrity: sha512-Hrp5vIK/xr5SkeN2onO32H0MgNZ0f17HRNH39WfL0SYUNOTZ5Lz1TJ8Pajo/87dYGEFlLMm7mIc/k/s6Bvz9HQ==} engines: {node: '>=8'} @@ -4157,6 +4320,12 @@ packages: hasBin: true dev: false + /highlight.js/9.18.5: + resolution: {integrity: sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==} + deprecated: Support has ended for 9.x series. Upgrade to @latest + requiresBuild: true + dev: false + /homedir-polyfill/1.0.3: resolution: {integrity: sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==} engines: {node: '>=0.10.0'} @@ -4474,6 +4643,11 @@ packages: resolution: {integrity: sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA==} dev: false + /is-stream/1.1.0: + resolution: {integrity: sha1-EtSj3U5o4Lec6428hBc66A2RykQ=} + engines: {node: '>=0.10.0'} + dev: false + /is-stream/2.0.1: resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} engines: {node: '>=8'} @@ -4557,11 +4731,23 @@ packages: resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} dev: false + /istanbul-lib-coverage/2.0.5: + resolution: {integrity: sha512-8aXznuEPCJvGnMSRft4udDRDtb1V3pkQkMMI5LI+6HuQz5oQ4J2UFn1H82raA3qJtyOLkkwVqICBQkjnGtn5mA==} + engines: {node: '>=6'} + dev: false + /istanbul-lib-coverage/3.2.0: resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} engines: {node: '>=8'} dev: false + /istanbul-lib-hook/2.0.7: + resolution: {integrity: sha512-vrRztU9VRRFDyC+aklfLoeXyNdTfga2EI3udDGn4cZ6fpSXpHLV9X6CHvfoMCPtggg8zvDDmC4b9xfu0z6/llA==} + engines: {node: '>=6'} + dependencies: + append-transform: 1.0.0 + dev: false + /istanbul-lib-hook/3.0.0: resolution: {integrity: sha512-Pt/uge1Q9s+5VAZ+pCo16TYMWPBIl+oaNIjgLQxcX0itS6ueeaA+pEfThZpH8WxhFgCiEb8sAJY6MdUKgiIWaQ==} engines: {node: '>=8'} @@ -4569,6 +4755,21 @@ packages: append-transform: 2.0.0 dev: false + /istanbul-lib-instrument/3.3.0: + resolution: {integrity: sha512-5nnIN4vo5xQZHdXno/YDXJ0G+I3dAm4XgzfSVTPLQpj/zAV2dV6Juy0yaf10/zrJOJeHoN3fraFe+XRq2bFVZA==} + engines: {node: '>=6'} + dependencies: + '@babel/generator': 7.16.5 + '@babel/parser': 7.16.6 + '@babel/template': 7.16.0 + '@babel/traverse': 7.16.5 + '@babel/types': 7.16.0 + istanbul-lib-coverage: 2.0.5 + semver: 6.3.0 + transitivePeerDependencies: + - supports-color + dev: false + /istanbul-lib-instrument/4.0.3: resolution: {integrity: sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==} engines: {node: '>=8'} @@ -4594,6 +4795,15 @@ packages: uuid: 3.4.0 dev: false + /istanbul-lib-report/2.0.8: + resolution: {integrity: sha512-fHBeG573EIihhAblwgxrSenp0Dby6tJMFR/HvlerBsrCTD5bkUuoNtn3gVh29ZCS824cGGBPn7Sg7cNk+2xUsQ==} + engines: {node: '>=6'} + dependencies: + istanbul-lib-coverage: 2.0.5 + make-dir: 2.1.0 + supports-color: 6.1.0 + dev: false + /istanbul-lib-report/3.0.0: resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} engines: {node: '>=8'} @@ -4603,6 +4813,19 @@ packages: supports-color: 7.2.0 dev: false + /istanbul-lib-source-maps/3.0.6: + resolution: {integrity: sha512-R47KzMtDJH6X4/YW9XTx+jrLnZnscW4VpNN+1PViSYTejLVPWv7oov+Duf8YQSPyVRUvueQqz1TcsC6mooZTXw==} + engines: {node: '>=6'} + dependencies: + debug: 4.3.3 + istanbul-lib-coverage: 2.0.5 + make-dir: 2.1.0 + rimraf: 2.7.1 + source-map: 0.6.1 + transitivePeerDependencies: + - supports-color + dev: false + /istanbul-lib-source-maps/4.0.1: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} @@ -4614,6 +4837,13 @@ packages: - supports-color dev: false + /istanbul-reports/2.2.7: + resolution: {integrity: sha512-uu1F/L1o5Y6LzPVSVZXNOoD/KXpJue9aeLRd0sM9uMXfZvzomB0WxVamWb5ue8kA2vVWEmW7EG+A5n3f1kqHKg==} + engines: {node: '>=6'} + dependencies: + html-escaper: 2.0.2 + dev: false + /istanbul-reports/3.1.1: resolution: {integrity: sha512-q1kvhAXWSsXfMjCdNHNPKZZv94OlspKnoGv+R9RGbnqOOQ0VbNfLFgQDVgi7hHenKsndGq3/o0OBdzDXthWcNw==} engines: {node: '>=8'} @@ -4639,6 +4869,10 @@ packages: resolution: {integrity: sha1-o6vicYryQaKykE+EpiWXDzia4yo=} dev: false + /jquery/3.6.0: + resolution: {integrity: sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==} + dev: false + /js-tokens/4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} dev: false @@ -5111,6 +5345,13 @@ packages: resolution: {integrity: sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==} dev: false + /lru-cache/4.1.5: + resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} + dependencies: + pseudomap: 1.0.2 + yallist: 2.1.2 + dev: false + /lru-cache/6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} @@ -5118,6 +5359,10 @@ packages: yallist: 4.0.0 dev: false + /lunr/2.3.9: + resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + dev: false + /machina/4.0.2: resolution: {integrity: sha512-OOlFrW1rd783S6tF36v5Ie/TM64gfvSl9kYLWL2cPA31J71HHWW3XrgSe1BZSFAPkh8532CMJMLv/s9L2aopiA==} engines: {node: '>=0.4.0'} @@ -5131,6 +5376,14 @@ packages: sourcemap-codec: 1.4.8 dev: false + /make-dir/2.1.0: + resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} + engines: {node: '>=6'} + dependencies: + pify: 4.0.1 + semver: 5.7.1 + dev: false + /make-dir/3.1.0: resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} engines: {node: '>=8'} @@ -5142,6 +5395,12 @@ packages: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: false + /marked/0.7.0: + resolution: {integrity: sha512-c+yYdCZJQrsRjTPhUx7VKkApw9bwDkNbHUKo1ovgcfDjb2kc8rLuRbIFyXL5WOEUwzSSKo3IXpph2K6DqB/KZg==} + engines: {node: '>=0.10.0'} + hasBin: true + dev: false + /matched/1.0.2: resolution: {integrity: sha512-7ivM1jFZVTOOS77QsR+TtYHH0ecdLclMkqbf5qiJdX2RorqfhsL65QHySPZgDE0ZjHoh+mQUNHTanNXIlzXd0Q==} engines: {node: '>= 0.12.0'} @@ -5176,6 +5435,12 @@ packages: resolution: {integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=} dev: false + /merge-source-map/1.1.0: + resolution: {integrity: sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==} + dependencies: + source-map: 0.6.1 + dev: false + /merge-stream/2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: false @@ -5270,6 +5535,19 @@ packages: hasBin: true dev: false + /mocha-junit-reporter/1.23.3_mocha@7.2.0: + resolution: {integrity: sha512-ed8LqbRj1RxZfjt/oC9t12sfrWsjZ3gNnbhV1nuj9R/Jb5/P3Xb4duv2eCfCDMYH+fEu0mqca7m4wsiVjsxsvA==} + peerDependencies: + mocha: '>=2.2.5' + dependencies: + debug: 2.6.9 + md5: 2.3.0 + mkdirp: 0.5.5 + mocha: 7.2.0 + strip-ansi: 4.0.0 + xml: 1.0.1 + dev: false + /mocha-junit-reporter/2.0.2_mocha@7.2.0: resolution: {integrity: sha512-vYwWq5hh3v1lG0gdQCBxwNipBfvDiAM1PHroQRNp96+2l72e9wEUTw+mzoK+O0SudgfQ7WvTQZ9Nh3qkAYAjfg==} peerDependencies: @@ -5381,6 +5659,14 @@ packages: engines: {node: '>= 0.6'} dev: false + /neo-async/2.6.2: + resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} + dev: false + + /nested-error-stacks/2.1.0: + resolution: {integrity: sha512-AO81vsIO1k1sM4Zrd6Hu7regmJN1NSiAja10gc4bX3F0wd+9rQmcuHQaHVQCYIEC8iFXnE+mavh23GOt7wBgug==} + dev: false + /nice-try/1.0.5: resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==} dev: false @@ -5395,6 +5681,16 @@ packages: path-to-regexp: 1.8.0 dev: false + /nise/5.1.0: + resolution: {integrity: sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==} + dependencies: + '@sinonjs/commons': 1.8.3 + '@sinonjs/fake-timers': 7.1.2 + '@sinonjs/text-encoding': 0.7.1 + just-extend: 4.2.1 + path-to-regexp: 1.8.0 + dev: false + /nock/12.0.3: resolution: {integrity: sha512-QNb/j8kbFnKCiyqi9C5DD0jH/FubFGj5rt9NQFONXwQm3IPB0CULECg/eS3AU1KgZb/6SwUa4/DTRKhVxkGABw==} engines: {node: '>= 10.13'} @@ -5516,6 +5812,40 @@ packages: engines: {node: '>=0.10.0'} dev: false + /nyc/14.1.1: + resolution: {integrity: sha512-OI0vm6ZGUnoGZv/tLdZ2esSVzDwUC88SNs+6JoSOMVxA+gKMB8Tk7jBwgemLx4O40lhhvZCVw1C+OYLOBOPXWw==} + engines: {node: '>=6'} + hasBin: true + dependencies: + archy: 1.0.0 + caching-transform: 3.0.2 + convert-source-map: 1.8.0 + cp-file: 6.2.0 + find-cache-dir: 2.1.0 + find-up: 3.0.0 + foreground-child: 1.5.6 + glob: 7.2.0 + istanbul-lib-coverage: 2.0.5 + istanbul-lib-hook: 2.0.7 + istanbul-lib-instrument: 3.3.0 + istanbul-lib-report: 2.0.8 + istanbul-lib-source-maps: 3.0.6 + istanbul-reports: 2.2.7 + js-yaml: 3.14.1 + make-dir: 2.1.0 + merge-source-map: 1.1.0 + resolve-from: 4.0.0 + rimraf: 2.7.1 + signal-exit: 3.0.6 + spawn-wrap: 1.4.3 + test-exclude: 5.2.3 + uuid: 3.4.0 + yargs: 13.3.2 + yargs-parser: 13.1.2 + transitivePeerDependencies: + - supports-color + dev: false + /nyc/15.1.0: resolution: {integrity: sha512-jMW04n9SxKdKi1ZMGhvUTHBN0EICCRkHemEoE5jm6mTYcqcdas0ATzgUgejlQUHMvpnOZqGB5Xxsv9KxJW1j8A==} engines: {node: '>=8.9'} @@ -5660,6 +5990,11 @@ packages: word-wrap: 1.2.3 dev: false + /os-homedir/1.0.2: + resolution: {integrity: sha1-/7xJiDNuDoM94MFox+8VISGqf7M=} + engines: {node: '>=0.10.0'} + dev: false + /p-limit/1.3.0: resolution: {integrity: sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==} engines: {node: '>=4'} @@ -5712,6 +6047,16 @@ packages: engines: {node: '>=6'} dev: false + /package-hash/3.0.0: + resolution: {integrity: sha512-lOtmukMDVvtkL84rJHI7dpTYq+0rli8N2wlnqUcBuDWCfVhRUfOmnR9SsoHFMLpACvEV60dX7rd0rFaYDZI+FA==} + engines: {node: '>=6'} + dependencies: + graceful-fs: 4.2.8 + hasha: 3.0.0 + lodash.flattendeep: 4.4.0 + release-zalgo: 1.0.0 + dev: false + /package-hash/4.0.0: resolution: {integrity: sha512-whdkPIooSu/bASggZ96BWVvZTRMOFxnyUG5PnTSGKoJE2gd5mbVNmR2Nj20QFzxYYgAXpoqC+AiXzl+UMRh7zQ==} engines: {node: '>=8'} @@ -5835,6 +6180,11 @@ packages: engines: {node: '>=4'} dev: false + /pify/4.0.1: + resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} + engines: {node: '>=6'} + dev: false + /pkg-dir/2.0.0: resolution: {integrity: sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=} engines: {node: '>=4'} @@ -5842,6 +6192,13 @@ packages: find-up: 2.1.0 dev: false + /pkg-dir/3.0.0: + resolution: {integrity: sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==} + engines: {node: '>=6'} + dependencies: + find-up: 3.0.0 + dev: false + /pkg-dir/4.2.0: resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==} engines: {node: '>=8'} @@ -5944,6 +6301,10 @@ packages: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} dev: false + /pseudomap/1.0.2: + resolution: {integrity: sha1-8FKijacOYYkX7wqKw0wa5aaChrM=} + dev: false + /psl/1.8.0: resolution: {integrity: sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==} dev: false @@ -6059,6 +6420,14 @@ packages: strip-json-comments: 2.0.1 dev: false + /read-pkg-up/4.0.0: + resolution: {integrity: sha512-6etQSH7nJGsK0RbG/2TeDzZFa8shjQ1um+SwQQ5cwKy0dhSXdOncEhb1CPpvQG4h7FyOV6EB6YlV0yJvZQNAkA==} + engines: {node: '>=6'} + dependencies: + find-up: 3.0.0 + read-pkg: 3.0.0 + dev: false + /read-pkg/3.0.0: resolution: {integrity: sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=} engines: {node: '>=4'} @@ -6240,6 +6609,13 @@ packages: debug: 3.2.7 dev: false + /rimraf/2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.0 + dev: false + /rimraf/3.0.2: resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} hasBin: true @@ -6487,6 +6863,17 @@ packages: simple-concat: 1.0.1 dev: false + /sinon/12.0.1: + resolution: {integrity: sha512-iGu29Xhym33ydkAT+aNQFBINakjq69kKO6ByPvTsm3yyIACfyQttRTP03aBP/I8GfhFmLzrnKwNNkr0ORb1udg==} + dependencies: + '@sinonjs/commons': 1.8.3 + '@sinonjs/fake-timers': 8.1.0 + '@sinonjs/samsam': 6.0.2 + diff: 5.0.0 + nise: 5.1.0 + supports-color: 7.2.0 + dev: false + /sinon/9.2.4: resolution: {integrity: sha512-zljcULZQsJxVra28qIAL6ow1Z9tpattkCTEJR4RBP3TGc00FcttsP5pK284Nas5WjMZU5Yzy3kAIp3B3KRf5Yg==} dependencies: @@ -6643,6 +7030,17 @@ packages: resolution: {integrity: sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A=} dev: false + /spawn-wrap/1.4.3: + resolution: {integrity: sha512-IgB8md0QW/+tWqcavuFgKYR/qIRvJkRLPJDFaoXtLLUaVcCDK0+HeFTkmQHj3eprcYhc+gOl0aEA1w7qZlYezw==} + dependencies: + foreground-child: 1.5.6 + mkdirp: 0.5.5 + os-homedir: 1.0.2 + rimraf: 2.7.1 + signal-exit: 3.0.6 + which: 1.3.1 + dev: false + /spawn-wrap/2.0.0: resolution: {integrity: sha512-EeajNjfN9zMnULLwhZZQU3GWBoFNkbngTUPfaawT4RkMiviTxcX0qfhVbGey39mfctfDHkWtuecgQ8NJcyQWHg==} engines: {node: '>=8'} @@ -6929,6 +7327,16 @@ packages: source-map-support: 0.5.21 dev: false + /test-exclude/5.2.3: + resolution: {integrity: sha512-M+oxtseCFO3EDtAaGH7iiej3CBkzXqFMbzqYAACdzKui4eZA+pq3tZEwChvOdNfa7xxy8BfbmgJSIr43cC/+2g==} + engines: {node: '>=6'} + dependencies: + glob: 7.2.0 + minimatch: 3.0.4 + read-pkg-up: 4.0.0 + require-main-filename: 2.0.0 + dev: false + /test-exclude/6.0.0: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} @@ -7172,6 +7580,46 @@ packages: is-typedarray: 1.0.0 dev: false + /typedoc-default-themes/0.6.3: + resolution: {integrity: sha512-rouf0TcIA4M2nOQFfC7Zp4NEwoYiEX4vX/ZtudJWU9IHA29MPC+PPgSXYLPESkUo7FuB//GxigO3mk9Qe1xp3Q==} + engines: {node: '>= 8'} + dependencies: + backbone: 1.4.0 + jquery: 3.6.0 + lunr: 2.3.9 + underscore: 1.13.2 + dev: false + + /typedoc/0.15.2: + resolution: {integrity: sha512-K2nFEtyDQTVdXOzYtECw3TwuT3lM91Zc0dzGSLuor5R8qzZbwqBoCw7xYGVBow6+mEZAvKGznLFsl7FzG+wAgQ==} + engines: {node: '>= 6.0.0'} + hasBin: true + dependencies: + '@types/minimatch': 3.0.3 + fs-extra: 8.1.0 + handlebars: 4.7.7 + highlight.js: 9.18.5 + lodash: 4.17.21 + marked: 0.7.0 + minimatch: 3.0.4 + progress: 2.0.3 + shelljs: 0.8.4 + typedoc-default-themes: 0.6.3 + typescript: 3.7.7 + dev: false + + /typescript/3.7.7: + resolution: {integrity: sha512-MmQdgo/XenfZPvVLtKZOq9jQQvzaUAUpcKW8Z43x9B2fOm4S5g//tPtMweZUIP+SoBqrVPEIm+dJeQ9dfO0QdA==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: false + + /typescript/3.9.10: + resolution: {integrity: sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: false + /typescript/4.2.4: resolution: {integrity: sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==} engines: {node: '>=4.2.0'} @@ -7216,6 +7664,10 @@ packages: through: 2.3.8 dev: false + /underscore/1.13.2: + resolution: {integrity: sha512-ekY1NhRzq0B08g4bGuX4wd2jZx5GnKz6mKSqFL4nqBlfyMGiG10gDFhDTMEfYmDL6Jy0FUIZp7wiRB+0BP7J2g==} + dev: false + /universal-user-agent/6.0.0: resolution: {integrity: sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==} dev: false @@ -7393,6 +7845,10 @@ packages: engines: {node: '>=0.10.0'} dev: false + /wordwrap/1.0.0: + resolution: {integrity: sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=} + dev: false + /wrap-ansi/5.1.0: resolution: {integrity: sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==} engines: {node: '>=6'} @@ -7424,6 +7880,14 @@ packages: resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} dev: false + /write-file-atomic/2.4.3: + resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} + dependencies: + graceful-fs: 4.2.8 + imurmurhash: 0.1.4 + signal-exit: 3.0.6 + dev: false + /write-file-atomic/3.0.3: resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} dependencies: @@ -7516,6 +7980,10 @@ packages: engines: {node: '>=10'} dev: false + /yallist/2.1.2: + resolution: {integrity: sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=} + dev: false + /yallist/4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} dev: false @@ -10873,7 +11341,7 @@ packages: dev: false file:projects/core-tracing.tgz: - resolution: {integrity: sha512-0an4uRa/vsk2n5k4n7uCz2ElXpveIyRVQFmXaaGC0eE02K1m4ts0JBD3CJEWsGT4hWzjed+ph+vCoyvRZS5irw==, tarball: file:projects/core-tracing.tgz} + resolution: {integrity: sha512-jbrBdV2RkHhskzce4H2Mr5DlutfswJErgAZvLAvlswMGwVF6toTtCGZcAaxFQpZzcOkxWVTEyma42v65lPDN6A==, tarball: file:projects/core-tracing.tgz} name: '@rush-temp/core-tracing' version: 0.0.0 dependencies: @@ -12145,6 +12613,57 @@ packages: - utf-8-validate dev: false + file:projects/opentelemetry-instrumentation-azure-sdk.tgz: + resolution: {integrity: sha512-Dz8EkDVTh8SfKUFatcu/WZUX6YQjIQaE3A1qmM9RPSGXloRh/3TmyrrqqKvP6WRoyYIosCqRbwvzMUdyPljP/g==, tarball: file:projects/opentelemetry-instrumentation-azure-sdk.tgz} + name: '@rush-temp/opentelemetry-instrumentation-azure-sdk' + version: 0.0.0 + dependencies: + '@microsoft/api-extractor': 7.19.2 + '@opentelemetry/api': 1.0.3 + '@opentelemetry/core': 1.0.1_@opentelemetry+api@1.0.3 + '@opentelemetry/instrumentation': 0.27.0_@opentelemetry+api@1.0.3 + '@types/chai': 4.3.0 + '@types/mocha': 7.0.2 + '@types/node': 12.20.40 + '@types/sinon': 10.0.6 + chai: 4.3.4 + cross-env: 7.0.3 + dotenv: 8.6.0 + downlevel-dts: 0.4.0 + eslint: 7.32.0 + esm: 3.2.25 + inherits: 2.0.4 + karma: 6.3.9 + karma-chrome-launcher: 3.1.0 + karma-coverage: 2.1.0 + karma-edge-launcher: 0.4.2_karma@6.3.9 + karma-env-preprocessor: 0.1.1 + karma-firefox-launcher: 1.3.0 + karma-ie-launcher: 1.0.0_karma@6.3.9 + karma-json-preprocessor: 0.3.3_karma@6.3.9 + karma-json-to-file-reporter: 1.0.1 + karma-junit-reporter: 2.0.1_karma@6.3.9 + karma-mocha: 2.0.1 + karma-mocha-reporter: 2.2.5_karma@6.3.9 + mocha: 7.2.0 + mocha-junit-reporter: 1.23.3_mocha@7.2.0 + nyc: 14.1.1 + prettier: 2.5.1 + rimraf: 3.0.2 + rollup: 1.32.1 + sinon: 12.0.1 + source-map-support: 0.5.21 + tslib: 2.3.1 + typedoc: 0.15.2 + typescript: 4.2.4 + util: 0.12.4 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + dev: false + file:projects/perf-ai-form-recognizer.tgz: resolution: {integrity: sha512-0VjfzGd47tkR/d9bIMi7fhm4V421vHWjp9AHAzIfMRT7rey5JsFzdpeJObqUc6jnA4n+xXq8HRgqVnibfzQXfw==, tarball: file:projects/perf-ai-form-recognizer.tgz} name: '@rush-temp/perf-ai-form-recognizer' @@ -13855,7 +14374,7 @@ packages: dev: false file:projects/test-utils.tgz: - resolution: {integrity: sha512-lRA7wxoXX7NWHc36e1+U8RH2RQmoZlFm50QXbLBRXqr+A8eIi8u5lphwOZchx6UzOpv1AatfESPoxcp5n7iyzA==, tarball: file:projects/test-utils.tgz} + resolution: {integrity: sha512-CEWMLlxQTpRRiRBCJi1fRyGOlLMmxCgd4C/5JwIekQL4LtpQ6OvRVGyJFSpJ4Y6dYxAwXyYnEF9MWrDWURAtIQ==, tarball: file:projects/test-utils.tgz} name: '@rush-temp/test-utils' version: 0.0.0 dependencies: diff --git a/rush.json b/rush.json index 56c67fcb0a82..e66a6aa1e3fe 100644 --- a/rush.json +++ b/rush.json @@ -606,6 +606,11 @@ "projectFolder": "sdk/core/logger", "versionPolicyName": "core" }, + { + "packageName": "@azure/opentelemetry-instrumentation-azure-sdk", + "projectFolder": "sdk/instrumentation/opentelemetry-instrumentation-azure-sdk", + "versionPolicyName": "client" + }, { "packageName": "@azure/schema-registry", "projectFolder": "sdk/schemaregistry/schema-registry", @@ -1302,4 +1307,4 @@ "versionPolicyName": "management" } ] -} \ No newline at end of file +} diff --git a/sdk/core/core-auth/review/core-auth.api.md b/sdk/core/core-auth/review/core-auth.api.md index d477c52a7114..6f6c12e825f9 100644 --- a/sdk/core/core-auth/review/core-auth.api.md +++ b/sdk/core/core-auth/review/core-auth.api.md @@ -34,13 +34,6 @@ export class AzureSASCredential implements SASCredential { update(newSignature: string): void; } -// @public -export interface Context { - deleteValue(key: symbol): Context; - getValue(key: symbol): unknown; - setValue(key: symbol, value: unknown): Context; -} - // @public export interface GetTokenOptions { abortSignal?: AbortSignalLike; @@ -49,7 +42,7 @@ export interface GetTokenOptions { }; tenantId?: string; tracingOptions?: { - tracingContext?: Context; + tracingContext?: TracingContext; }; } @@ -83,6 +76,12 @@ export interface TokenCredential { getToken(scopes: string | string[], options?: GetTokenOptions): Promise; } +// @public +export interface TracingContext { + deleteValue(key: symbol): TracingContext; + getValue(key: symbol): unknown; + setValue(key: symbol, value: unknown): TracingContext; +} // (No @packageDocumentation comment for this package) diff --git a/sdk/core/core-auth/src/index.ts b/sdk/core/core-auth/src/index.ts index a44b072ce99e..d9345b49e750 100644 --- a/sdk/core/core-auth/src/index.ts +++ b/sdk/core/core-auth/src/index.ts @@ -16,4 +16,4 @@ export { isTokenCredential, } from "./tokenCredential"; -export { Context } from "./tracing"; +export { TracingContext } from "./tracing"; diff --git a/sdk/core/core-auth/src/tokenCredential.ts b/sdk/core/core-auth/src/tokenCredential.ts index ef8f0e04a3e6..2d8416b9cc1f 100644 --- a/sdk/core/core-auth/src/tokenCredential.ts +++ b/sdk/core/core-auth/src/tokenCredential.ts @@ -2,7 +2,7 @@ // Licensed under the MIT license. import { AbortSignalLike } from "@azure/abort-controller"; -import { Context } from "./tracing"; +import { TracingContext } from "./tracing"; /** * Represents a credential capable of providing an authentication token. @@ -43,9 +43,9 @@ export interface GetTokenOptions { */ tracingOptions?: { /** - * OpenTelemetry context + * Tracing Context for the current request. */ - tracingContext?: Context; + tracingContext?: TracingContext; }; /** diff --git a/sdk/core/core-auth/src/tracing.ts b/sdk/core/core-auth/src/tracing.ts index 3d8958fa8817..49a4cfe66cd7 100644 --- a/sdk/core/core-auth/src/tracing.ts +++ b/sdk/core/core-auth/src/tracing.ts @@ -7,7 +7,7 @@ /** * An interface structurally compatible with OpenTelemetry. */ -export interface Context { +export interface TracingContext { /** * Get a value from the context. * @@ -21,12 +21,12 @@ export interface Context { * @param key - context key for which to set the value * @param value - value to set for the given key */ - setValue(key: symbol, value: unknown): Context; + setValue(key: symbol, value: unknown): TracingContext; /** * Return a new context which inherits from this context but does * not contain a value for the given key. * * @param key - context key for which to clear a value */ - deleteValue(key: symbol): Context; + deleteValue(key: symbol): TracingContext; } diff --git a/sdk/core/core-tracing/CHANGELOG.md b/sdk/core/core-tracing/CHANGELOG.md index 43a39aa57f24..1c76921c21dd 100644 --- a/sdk/core/core-tracing/CHANGELOG.md +++ b/sdk/core/core-tracing/CHANGELOG.md @@ -6,8 +6,11 @@ ### Breaking Changes -- SpanOptions has been removed from OperationTracingOptions as it is internal and should not be exposed by client libraries. - - Customizing a newly created Span is only supported via passing `SpanOptions` to `createSpanFunction` +- @azure/core-tracing has been rewritten in order to provide cleaner abstractions for client libraries as well as remove @opentelemetry/api as a direct dependency. + - @opentelemetry/api is no longer a direct dependency of @azure/core-tracing providing for smaller bundle sizes and lower incidence of version conflicts + - `createSpanFunction` has been removed and replaced with a stateful `TracingClient` which can be created using the `createTracingClient` function. + - `TracingClient` introduces a new API for creating tracing spans. Use `TracingClient#withSpan` to wrap an invocation in a span, ensuring the span is ended and exceptions are captured. + - `TracingClient` also provides the lower-level APIs necessary to start a span without making it active, create request headers, serialize `traceparent` header, and wrapping a callback with an active context. ### Bugs Fixed diff --git a/sdk/core/core-tracing/package.json b/sdk/core/core-tracing/package.json index f5c12a00573b..8525b70d8fd6 100644 --- a/sdk/core/core-tracing/package.json +++ b/sdk/core/core-tracing/package.json @@ -5,9 +5,7 @@ "sdk-type": "client", "main": "dist/index.js", "module": "dist-esm/src/index.js", - "browser": { - "./dist-esm/src/utils/global.js": "./dist-esm/src/utils/global.browser.js" - }, + "browser": {}, "react-native": { "./dist/index.js": "./dist-esm/src/index.js" }, @@ -59,7 +57,6 @@ "homepage": "https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/core/core-tracing/README.md", "sideEffects": false, "dependencies": { - "@opentelemetry/api": "^1.0.1", "tslib": "^2.2.0" }, "devDependencies": { @@ -67,7 +64,6 @@ "@azure/dev-tool": "^1.0.0", "@azure/eslint-plugin-azure-sdk": "^3.0.0", "@microsoft/api-extractor": "^7.18.11", - "@opentelemetry/tracing": "^0.22.0", "@types/chai": "^4.1.6", "@types/mocha": "^7.0.2", "@types/node": "^12.0.0", diff --git a/sdk/core/core-tracing/review/core-tracing.api.md b/sdk/core/core-tracing/review/core-tracing.api.md index 82584f47f709..d4f53be814e2 100644 --- a/sdk/core/core-tracing/review/core-tracing.api.md +++ b/sdk/core/core-tracing/review/core-tracing.api.md @@ -5,183 +5,106 @@ ```ts // @public -export interface Context { - deleteValue(key: symbol): Context; - getValue(key: symbol): unknown; - setValue(key: symbol, value: unknown): Context; -} - -// @public -const context_2: ContextAPI; -export { context_2 as context } - -// @public -export interface ContextAPI { - active(): Context; -} +export function createTracingClient(options: TracingClientOptions): TracingClient; // @public -export function createSpanFunction(args: CreateSpanFunctionArgs): (operationName: string, operationOptions?: T | undefined, startSpanOptions?: SpanOptions | undefined) => { - span: Span; - updatedOptions: T; -}; - -// @public -export interface CreateSpanFunctionArgs { - namespace: string; - packagePrefix: string; +export interface Instrumenter { + createRequestHeaders(tracingContext?: TracingContext): Record; + parseTraceparentHeader(traceparentHeader: string): TracingContext | undefined; + startSpan(name: string, spanOptions: InstrumenterSpanOptions): { + span: TracingSpan; + tracingContext: TracingContext; + }; + withContext ReturnType>(context: TracingContext, callback: Callback, ...callbackArgs: CallbackArgs): ReturnType; } // @public -export type Exception = ExceptionWithCode | ExceptionWithMessage | ExceptionWithName | string; - -// @public -export interface ExceptionWithCode { - code: string | number; - message?: string; - name?: string; - stack?: string; -} - -// @public -export interface ExceptionWithMessage { - code?: string | number; - message: string; - name?: string; - stack?: string; -} - -// @public -export interface ExceptionWithName { - code?: string | number; - message?: string; - name: string; - stack?: string; -} - -// @public -export function extractSpanContextFromTraceParentHeader(traceParentHeader: string): SpanContext | undefined; - -// @public -export function getSpan(context: Context): Span | undefined; - -// @public -export function getSpanContext(context: Context): SpanContext | undefined; - -// @public -export function getTraceParentHeader(spanContext: SpanContext): string | undefined; - -// @public -export function getTracer(): Tracer; - -// @public -export function getTracer(name: string, version?: string): Tracer; - -// @public -export type HrTime = [number, number]; - -// @public -export function isSpanContextValid(context: SpanContext): boolean; - -// @public -export interface Link { - attributes?: SpanAttributes; - context: SpanContext; +export interface InstrumenterSpanOptions extends TracingSpanOptions { + packageName: string; + packageVersion?: string; + tracingContext?: TracingContext; } // @public export interface OperationTracingOptions { - tracingContext?: Context; + tracingContext?: TracingContext; } // @public -export function setSpan(context: Context, span: Span): Context; +export type SpanStatus = SpanStatusSuccess | SpanStatusError; // @public -export function setSpanContext(context: Context, spanContext: SpanContext): Context; - -// @public -export interface Span { - addEvent(name: string, attributesOrStartTime?: SpanAttributes | TimeInput, startTime?: TimeInput): this; - end(endTime?: TimeInput): void; - isRecording(): boolean; - recordException(exception: Exception, time?: TimeInput): void; - setAttribute(key: string, value: SpanAttributeValue): this; - setAttributes(attributes: SpanAttributes): this; - setStatus(status: SpanStatus): this; - spanContext(): SpanContext; - updateName(name: string): this; -} - -// @public -export interface SpanAttributes { - [attributeKey: string]: SpanAttributeValue | undefined; -} - -// @public -export type SpanAttributeValue = string | number | boolean | Array | Array | Array; +export type SpanStatusError = { + status: "error"; + error?: Error | string; +}; // @public -export interface SpanContext { - spanId: string; - traceFlags: number; - traceId: string; - traceState?: TraceState; -} +export type SpanStatusSuccess = { + status: "success"; +}; // @public -export enum SpanKind { - CLIENT = 2, - CONSUMER = 4, - INTERNAL = 0, - PRODUCER = 3, - SERVER = 1 +export interface TracingClient { + createRequestHeaders(tracingContext?: TracingContext): Record; + parseTraceparentHeader(traceparentHeader: string): TracingContext | undefined; + startSpan(name: string, operationOptions?: Options, spanOptions?: TracingSpanOptions): { + span: TracingSpan; + updatedOptions: Options; + }; + withContext ReturnType>(context: TracingContext, callback: Callback, ...callbackArgs: CallbackArgs): ReturnType; + withSpan) => ReturnType>(name: string, operationOptions: Options, callback: Callback, spanOptions?: TracingSpanOptions): Promise>; } // @public -export interface SpanOptions { - attributes?: SpanAttributes; - kind?: SpanKind; - links?: Link[]; - startTime?: TimeInput; +export interface TracingClientOptions { + namespace: string; + packageName: string; + packageVersion?: string; } // @public -export interface SpanStatus { - code: SpanStatusCode; - message?: string; +export interface TracingContext { + deleteValue(key: symbol): TracingContext; + getValue(key: symbol): unknown; + setValue(key: symbol, value: unknown): TracingContext; } // @public -export enum SpanStatusCode { - ERROR = 2, - OK = 1, - UNSET = 0 +export interface TracingSpan { + end(): void; + isRecording(): boolean; + recordException(exception: Error | string): void; + setAttribute(name: string, value: unknown): void; + setStatus(status: SpanStatus): void; } // @public -export type TimeInput = HrTime | number | Date; +export type TracingSpanKind = "client" | "server" | "producer" | "consumer" | "internal"; // @public -export const enum TraceFlags { - NONE = 0, - SAMPLED = 1 +export interface TracingSpanLink { + attributes?: { + [key: string]: unknown; + }; + tracingContext: TracingContext; } // @public -export interface Tracer { - startSpan(name: string, options?: SpanOptions, context?: Context): Span; +export interface TracingSpanOptions { + spanAttributes?: { + [key: string]: unknown; + }; + spanKind?: TracingSpanKind; + spanLinks?: TracingSpanLink[]; } // @public -export interface TraceState { - get(key: string): string | undefined; - serialize(): string; - set(key: string, value: string): TraceState; - unset(key: string): TraceState; -} +export function useInstrumenter(instrumenter: Instrumenter): void; // (No @packageDocumentation comment for this package) diff --git a/sdk/core/core-tracing/src/createSpan.ts b/sdk/core/core-tracing/src/createSpan.ts deleted file mode 100644 index f7fb4f76954d..000000000000 --- a/sdk/core/core-tracing/src/createSpan.ts +++ /dev/null @@ -1,215 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { - Context, - OperationTracingOptions, - Span, - SpanKind, - SpanOptions, - getTracer, - context as otContext, - setSpan, -} from "./interfaces"; -import { INVALID_SPAN_CONTEXT, trace } from "@opentelemetry/api"; - -/** - * Arguments for `createSpanFunction` that allow you to specify the - * prefix for each created span as well as the `az.namespace` attribute. - * - * @hidden - */ -export interface CreateSpanFunctionArgs { - /** - * Package name prefix. - * - * NOTE: if this is empty no prefix will be applied to created Span names. - */ - packagePrefix: string; - /** - * Service namespace - * - * NOTE: if this is empty no `az.namespace` attribute will be added to created Spans. - */ - namespace: string; -} - -/** - * @internal - * A set of known span attributes that will exist on a context - */ -export const knownSpanAttributes = { - AZ_NAMESPACE: { contextKey: Symbol.for("az.namespace"), spanAttributeName: "az.namespace" }, -}; - -/** - * Checks whether tracing is disabled by checking the `AZURE_TRACING_DISABLED` environment variable. - * - * @returns - `true` if tracing is disabled, `false` otherwise. - * - * @internal - */ -export function isTracingDisabled(): boolean { - if (typeof process === "undefined") { - // not supported in browser for now without polyfills - return false; - } - - const azureTracingDisabledValue = process.env.AZURE_TRACING_DISABLED?.toLowerCase(); - - if (azureTracingDisabledValue === "false" || azureTracingDisabledValue === "0") { - return false; - } - - return Boolean(azureTracingDisabledValue); -} - -/** - * Maintains backwards compatibility with the previous `OperationTracingOptions` in core-tracing preview.13 and earlier - * which passed `spanOptions` as part of `tracingOptions`. - */ -function disambiguateParameters( - operationOptions: T, - startSpanOptions?: SpanOptions -): [OperationTracingOptions, SpanOptions] { - const { tracingOptions } = operationOptions; - - // If startSpanOptions is provided, then we are using the new signature, - // otherwise try to pluck it out of the tracingOptions. - const spanOptions: SpanOptions = startSpanOptions || (tracingOptions as any)?.spanOptions || {}; - spanOptions.kind = spanOptions.kind || SpanKind.INTERNAL; - - return [tracingOptions || {}, spanOptions]; -} - -/** - * Creates a new span using the given parameters. - * - * @param spanName - The name of the span to created. - * @param spanOptions - Initialization options that can be used to customize the created span. - * @param tracingContext - The tracing context to use for the created span. - * - * @returns - A new span. - */ -function startSpan(spanName: string, spanOptions: SpanOptions, tracingContext: Context) { - if (isTracingDisabled()) { - return trace.wrapSpanContext(INVALID_SPAN_CONTEXT); - } - - const tracer = getTracer(); - return tracer.startSpan(spanName, spanOptions, tracingContext); -} - -/** - * Adds the `az.namespace` attribute on a span, the tracingContext, and the spanOptions - * - * @param span - The span to add the attribute to in place. - * @param tracingContext - The context bag to add the attribute to by creating a new context with the attribute. - * @param namespace - The value of the attribute. - * @param spanOptions - The spanOptions to add the attribute to (for backwards compatibility). - * - * @internal - * - * @returns The updated span options and context. - */ -function setNamespaceOnSpan( - span: Span, - tracingContext: Context, - namespace: string, - spanOptions: SpanOptions -) { - span.setAttribute(knownSpanAttributes.AZ_NAMESPACE.spanAttributeName, namespace); - const updatedContext = tracingContext.setValue( - knownSpanAttributes.AZ_NAMESPACE.contextKey, - namespace - ); - - // Here for backwards compatibility, but can be removed once we no longer use `spanOptions` (every client and core library depends on a version higher than preview.13) - const updatedSpanOptions = { - ...spanOptions, - attributes: { - ...spanOptions?.attributes, - [knownSpanAttributes.AZ_NAMESPACE.spanAttributeName]: namespace, - }, - }; - - return { updatedSpanOptions, updatedContext }; -} - -/** - * Creates a function that can be used to create spans using the global tracer. - * - * Usage: - * - * ```typescript - * // once - * const createSpan = createSpanFunction({ packagePrefix: "Azure.Data.AppConfiguration", namespace: "Microsoft.AppConfiguration" }); - * - * // in each operation - * const span = createSpan("deleteConfigurationSetting", operationOptions, startSpanOptions ); - * // code... - * span.end(); - * ``` - * - * @param args - allows configuration of the prefix for each span as well as the az.namespace field. - */ -export function createSpanFunction(args: CreateSpanFunctionArgs) { - /** - * Creates a span using the global tracer provider. - * - * @param operationName - The name of the operation to create a span for. - * @param operationOptions - The operation options containing the currently active tracing context when using manual span propagation. - * @param startSpanOptions - The options to use when creating the span, and will be passed to the tracer.startSpan method. - * - * @returns - A span from the global tracer provider, and an updatedOptions bag containing the new tracing context. - * - * Example usage: - * ```ts - * const { span, updatedOptions } = createSpan("deleteConfigurationSetting", operationOptions, startSpanOptions); - * ``` - */ - return function ( - operationName: string, - operationOptions?: T, - startSpanOptions?: SpanOptions - ): { span: Span; updatedOptions: T } { - const [tracingOptions, spanOptions] = disambiguateParameters( - operationOptions || ({} as T), - startSpanOptions - ); - - let tracingContext = tracingOptions?.tracingContext || otContext.active(); - - const spanName = args.packagePrefix ? `${args.packagePrefix}.${operationName}` : operationName; - const span = startSpan(spanName, spanOptions, tracingContext); - - let newSpanOptions = spanOptions; - if (args.namespace) { - const { updatedSpanOptions, updatedContext } = setNamespaceOnSpan( - span, - tracingContext, - args.namespace, - spanOptions - ); - - tracingContext = updatedContext; - newSpanOptions = updatedSpanOptions; - } - - const newTracingOptions = { - ...tracingOptions, - spanOptions: newSpanOptions, - tracingContext: setSpan(tracingContext, span), - }; - - const newOperationOptions = { - ...(operationOptions as T), - tracingOptions: newTracingOptions, - }; - - return { - span, - updatedOptions: newOperationOptions, - }; - }; -} diff --git a/sdk/core/core-tracing/src/index.ts b/sdk/core/core-tracing/src/index.ts index 9d76dba24cd8..423149e112f8 100644 --- a/sdk/core/core-tracing/src/index.ts +++ b/sdk/core/core-tracing/src/index.ts @@ -1,43 +1,20 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -// Tracers and wrappers -export { createSpanFunction, CreateSpanFunctionArgs } from "./createSpan"; - -// Shared interfaces export { - context, - Context, - ContextAPI, - Exception, - ExceptionWithCode, - ExceptionWithMessage, - ExceptionWithName, - getSpan, - getSpanContext, - getTracer, - HrTime, - isSpanContextValid, - Link, + Instrumenter, + InstrumenterSpanOptions, OperationTracingOptions, - setSpan, - setSpanContext, - Span, - SpanAttributes, - SpanAttributeValue, - SpanContext, - SpanKind, - SpanOptions, SpanStatus, - SpanStatusCode, - TimeInput, - TraceFlags, - Tracer, - TraceState, + SpanStatusError, + SpanStatusSuccess, + TracingClient, + TracingClientOptions, + TracingContext, + TracingSpan, + TracingSpanKind, + TracingSpanLink, + TracingSpanOptions, } from "./interfaces"; - -// Utilities -export { - extractSpanContextFromTraceParentHeader, - getTraceParentHeader, -} from "./utils/traceParentHeader"; +export { useInstrumenter } from "./instrumenter"; +export { createTracingClient } from "./tracingClient"; diff --git a/sdk/core/core-tracing/src/instrumenter.ts b/sdk/core/core-tracing/src/instrumenter.ts new file mode 100644 index 000000000000..b2898e6a387f --- /dev/null +++ b/sdk/core/core-tracing/src/instrumenter.ts @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { Instrumenter, InstrumenterSpanOptions, TracingContext, TracingSpan } from "./interfaces"; +import { createTracingContext } from "./tracingContext"; + +export function createDefaultTracingSpan(): TracingSpan { + return { + end: () => { + // noop + }, + isRecording: () => false, + recordException: () => { + // noop + }, + setAttribute: () => { + // noop + }, + setStatus: () => { + // noop + }, + }; +} + +export function createDefaultInstrumenter(): Instrumenter { + return { + createRequestHeaders: (): Record => { + return {}; + }, + parseTraceparentHeader: (): TracingContext | undefined => { + return undefined; + }, + startSpan: ( + _name: string, + spanOptions: InstrumenterSpanOptions + ): { span: TracingSpan; tracingContext: TracingContext } => { + return { + span: createDefaultTracingSpan(), + tracingContext: createTracingContext({ parentContext: spanOptions.tracingContext }), + }; + }, + withContext< + CallbackArgs extends unknown[], + Callback extends (...args: CallbackArgs) => ReturnType + >( + _context: TracingContext, + callback: Callback, + ...callbackArgs: CallbackArgs + ): ReturnType { + return callback(...callbackArgs); + }, + }; +} + +/** @internal */ +let instrumenterImplementation: Instrumenter | undefined; + +/** + * Extends the Azure SDK with support for a given instrumenter implementation. + * + * @param instrumenter - The instrumenter implementation to use. + */ +export function useInstrumenter(instrumenter: Instrumenter): void { + instrumenterImplementation = instrumenter; +} + +/** + * Gets the currently set instrumenter, a No-Op instrumenter by default. + * + * @returns The currently set instrumenter + */ +export function getInstrumenter(): Instrumenter { + if (!instrumenterImplementation) { + instrumenterImplementation = createDefaultInstrumenter(); + } + return instrumenterImplementation; +} diff --git a/sdk/core/core-tracing/src/interfaces.ts b/sdk/core/core-tracing/src/interfaces.ts index d5470dcb1416..acfec30a582d 100644 --- a/sdk/core/core-tracing/src/interfaces.ts +++ b/sdk/core/core-tracing/src/interfaces.ts @@ -1,500 +1,268 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import { context as otContext, trace as otTrace } from "@opentelemetry/api"; - /** - * A Tracer. + * Represents a client that can integrate with the currently configured {@link Instrumenter}. + * + * Create an instance using {@link createTracingClient}. */ -export interface Tracer { +export interface TracingClient { /** - * Starts a new {@link Span}. Start the span without setting it on context. + * Wraps a callback in a tracing span, calls the callback, and closes the span. * - * This method does NOT modify the current Context. + * This is the primary interface for using Tracing and will handle error recording as well as setting the status on the span. * - * @param name - The name of the span - * @param options - SpanOptions used for span creation - * @param context - Context to use to extract parent - * @returns The newly created span - * @example - * const span = tracer.startSpan('op'); - * span.setAttribute('key', 'value'); - * span.end(); - */ - startSpan(name: string, options?: SpanOptions, context?: Context): Span; -} - -/** - * TraceState. - */ -export interface TraceState { + * Example: + * + * ```ts + * const myOperationResult = await tracingClient.withSpan("myClassName.myOperationName", options, (updatedOptions) => myOperation(updatedOptions)); + * ``` + * @param name - The name of the span. By convention this should be `${className}.${methodName}`. + * @param operationOptions - The original options passed to the method. The callback will receive these options with the newly created {@link TracingContext}. + * @param callback - The callback to be invoked with the updated options and newly created {@link TracingSpan}. + */ + withSpan< + Options extends { tracingOptions?: OperationTracingOptions }, + Callback extends ( + updatedOptions: Options, + span: Omit + ) => ReturnType + >( + name: string, + operationOptions: Options, + callback: Callback, + spanOptions?: TracingSpanOptions + ): Promise>; /** - * Create a new TraceState which inherits from this TraceState and has the - * given key set. - * The new entry will always be added in the front of the list of states. + * Starts a given span but does not set it as the active span. + * + * You must end the span using {@link TracingSpan.end}. * - * @param key - key of the TraceState entry. - * @param value - value of the TraceState entry. + * Most of the time you will want to use {@link withSpan} instead. + * + * @param name - The name of the span. By convention this should be `${className}.${methodName}`. + * @param operationOptions - The original operation options. + * @param spanOptions - The options to use when creating the span. + * + * @returns A {@link TracingSpan} and the updated operation options. */ - set(key: string, value: string): TraceState; + startSpan( + name: string, + operationOptions?: Options, + spanOptions?: TracingSpanOptions + ): { span: TracingSpan; updatedOptions: Options }; /** - * Return a new TraceState which inherits from this TraceState but does not - * contain the given key. + * Wraps a callback with an active context and calls the callback. + * Depending on the implementation, this may set the globally available active context. * - * @param key - the key for the TraceState entry to be removed. + * Useful when you want to leave the boundaries of the SDK (make a request or callback to user code) and are unable to use the {@link withSpan} API. + * + * @param context - The {@link TracingContext} to use as the active context in the scope of the callback. + * @param callback - The callback to be invoked with the given context set as the globally active context. + * @param callbackArgs - The callback arguments. */ - unset(key: string): TraceState; + withContext< + CallbackArgs extends unknown[], + Callback extends (...args: CallbackArgs) => ReturnType + >( + context: TracingContext, + callback: Callback, + ...callbackArgs: CallbackArgs + ): ReturnType; + /** - * Returns the value to which the specified key is mapped, or `undefined` if - * this map contains no mapping for the key. + * Parses a traceparent header value into a {@link TracingSpanContext}. * - * @param key - with which the specified value is to be associated. - * @returns the value to which the specified key is mapped, or `undefined` if - * this map contains no mapping for the key. + * @param traceparentHeader - The traceparent header to parse. + * @returns An implementation-specific identifier for the span. */ - get(key: string): string | undefined; + parseTraceparentHeader(traceparentHeader: string): TracingContext | undefined; + /** - * Serializes the TraceState to a `list` as defined below. The `list` is a - * series of `list-members` separated by commas `,`, and a list-member is a - * key/value pair separated by an equals sign `=`. Spaces and horizontal tabs - * surrounding `list-members` are ignored. There can be a maximum of 32 - * `list-members` in a `list`. + * Creates a set of request headers to propagate tracing information to a backend. * - * @returns the serialized string. + * @param tracingContext - The context containing the span to propagate. + * @returns The set of headers to add to a request. */ - serialize(): string; + createRequestHeaders(tracingContext?: TracingContext): Record; } /** - * Represents high resolution time. + * Options that can be passed to {@link createTracingClient} */ -export declare type HrTime = [number, number]; +export interface TracingClientOptions { + /** The value of the az.namespace tracing attribute on newly created spans. */ + namespace: string; + /** The name of the package invoking this trace. */ + packageName: string; + /** An optional version of the package invoking this trace. */ + packageVersion?: string; +} -/** - * Used to represent a Time. - */ -export type TimeInput = HrTime | number | Date; +/** The kind of span. */ +export type TracingSpanKind = "client" | "server" | "producer" | "consumer" | "internal"; + +/** Options used to configure the newly created span. */ +export interface TracingSpanOptions { + /** The kind of span. Implementations should default this to "client". */ + spanKind?: TracingSpanKind; + /** A collection of {@link TracingSpanLink} to link to this span. */ + spanLinks?: TracingSpanLink[]; + /** Initial set of attributes to set on a span. */ + spanAttributes?: { [key: string]: unknown }; +} -/** - * The status for a span. - */ -export interface SpanStatus { - /** The status code of this message. */ - code: SpanStatusCode; - /** A developer-facing error message. */ - message?: string; +/** A pointer from the current {@link TracingSpan} to another span in the same or a different trace. */ +export interface TracingSpanLink { + /** The {@link TracingContext} containing the span context to link to. */ + tracingContext: TracingContext; + /** A set of attributes on the link. */ + attributes?: { [key: string]: unknown }; } /** - * The kind of span. + * Represents an implementation agnostic instrumenter. */ -export enum SpanKind { - /** Default value. Indicates that the span is used internally. */ - INTERNAL = 0, +export interface Instrumenter { /** - * Indicates that the span covers server-side handling of an RPC or other - * remote request. + * Creates a new {@link TracingSpan} with the given name and options and sets it on a new context. + * @param name - The name of the span. By convention this should be `${className}.${methodName}`. + * @param spanOptions - The options to use when creating the span. + * + * @returns A {@link TracingSpan} that can be used to end the span, and the context this span has been set on. */ - SERVER = 1, + startSpan( + name: string, + spanOptions: InstrumenterSpanOptions + ): { span: TracingSpan; tracingContext: TracingContext }; /** - * Indicates that the span covers the client-side wrapper around an RPC or - * other remote request. + * Wraps a callback with an active context and calls the callback. + * Depending on the implementation, this may set the globally available active context. + * + * @param context - The {@link TracingContext} to use as the active context in the scope of the callback. + * @param callback - The callback to be invoked with the given context set as the globally active context. + * @param callbackArgs - The callback arguments. */ - CLIENT = 2, + withContext< + CallbackArgs extends unknown[], + Callback extends (...args: CallbackArgs) => ReturnType + >( + context: TracingContext, + callback: Callback, + ...callbackArgs: CallbackArgs + ): ReturnType; + /** - * Indicates that the span describes producer sending a message to a - * broker. Unlike client and server, there is no direct critical path latency - * relationship between producer and consumer spans. + * Provides an implementation-specific method to parse a {@link https://www.w3.org/TR/trace-context/#traceparent-header} + * into a {@link TracingSpanContext} which can be used to link non-parented spans together. */ - PRODUCER = 3, + parseTraceparentHeader(traceparentHeader: string): TracingContext | undefined; /** - * Indicates that the span describes consumer receiving a message from a - * broker. Unlike client and server, there is no direct critical path latency - * relationship between producer and consumer spans. + * Provides an implementation-specific method to serialize a {@link TracingSpan} to a set of headers. + * @param tracingContext - The context containing the span to serialize. */ - CONSUMER = 4, + createRequestHeaders(tracingContext?: TracingContext): Record; } /** - * An Exception for a Span. - */ -export declare type Exception = - | ExceptionWithCode - | ExceptionWithMessage - | ExceptionWithName - | string; - -/** - * An Exception with a code. + * Options passed to {@link Instrumenter.startSpan} as a superset of {@link TracingSpanOptions}. */ -export interface ExceptionWithCode { - /** The code. */ - code: string | number; - /** The name. */ - name?: string; - /** The message. */ - message?: string; - /** The stack. */ - stack?: string; +export interface InstrumenterSpanOptions extends TracingSpanOptions { + /** The name of the package invoking this trace. */ + packageName: string; + /** The version of the package invoking this trace. */ + packageVersion?: string; + /** The current tracing context. Defaults to an implementation-specific "active" context. */ + tracingContext?: TracingContext; } /** - * An Exception with a message. + * Status representing a successful operation that can be sent to {@link TracingSpan.setStatus} */ -export interface ExceptionWithMessage { - /** The code. */ - code?: string | number; - /** The message. */ - message: string; - /** The name. */ - name?: string; - /** The stack. */ - stack?: string; -} +export type SpanStatusSuccess = { status: "success" }; /** - * An Exception with a name. + * Status representing an error that can be sent to {@link TracingSpan.setStatus} */ -export interface ExceptionWithName { - /** The code. */ - code?: string | number; - /** The message. */ - message?: string; - /** The name. */ - name: string; - /** The stack. */ - stack?: string; -} +export type SpanStatusError = { status: "error"; error?: Error | string }; /** - * Return the span if one exists + * Represents the statuses that can be passed to {@link TracingSpan.setStatus}. * - * @param context - context to get span from + * By default, all spans will be created with status "unset". */ -export function getSpan(context: Context): Span | undefined { - return otTrace.getSpan(context); -} +export type SpanStatus = SpanStatusSuccess | SpanStatusError; /** - * Set the span on a context - * - * @param context - context to use as parent - * @param span - span to set active - */ -export function setSpan(context: Context, span: Span): Context { - return otTrace.setSpan(context, span); -} - -/** - * Wrap span context in a NoopSpan and set as span in a new - * context - * - * @param context - context to set active span on - * @param spanContext - span context to be wrapped - */ -export function setSpanContext(context: Context, spanContext: SpanContext): Context { - return otTrace.setSpanContext(context, spanContext); -} - -/** - * Get the span context of the span if it exists. - * - * @param context - context to get values from - */ -export function getSpanContext(context: Context): SpanContext | undefined { - return otTrace.getSpanContext(context); -} - -/** - * Singleton object which represents the entry point to the OpenTelemetry Context API - */ -export interface ContextAPI { - /** - * Get the currently active context - */ - active(): Context; -} - -/** - * Returns true of the given {@link SpanContext} is valid. - * A valid {@link SpanContext} is one which has a valid trace ID and span ID as per the spec. - * - * @param context - the {@link SpanContext} to validate. - * - * @returns true if the {@link SpanContext} is valid, false otherwise. - */ -export function isSpanContextValid(context: SpanContext): boolean { - return otTrace.isSpanContextValid(context); -} - -/** - * Retrieves a tracer from the global tracer provider. - */ -export function getTracer(): Tracer; -/** - * Retrieves a tracer from the global tracer provider. - */ -export function getTracer(name: string, version?: string): Tracer; -export function getTracer(name?: string, version?: string): Tracer { - return otTrace.getTracer(name || "azure/core-tracing", version); -} - -/** Entrypoint for context API */ -export const context: ContextAPI = otContext; - -/** SpanStatusCode */ -export enum SpanStatusCode { - /** - * The default status. - */ - UNSET = 0, - /** - * The operation has been validated by an Application developer or - * Operator to have completed successfully. - */ - OK = 1, - /** - * The operation contains an error. - */ - ERROR = 2, -} - -/** - * An interface that represents a span. A span represents a single operation - * within a trace. Examples of span might include remote procedure calls or a - * in-process function calls to sub-components. A Trace has a single, top-level - * "root" Span that in turn may have zero or more child Spans, which in turn - * may have children. - * - * Spans are created by the {@link Tracer.startSpan} method. + * Represents an implementation agnostic tracing span. */ -export interface Span { +export interface TracingSpan { /** - * Returns the {@link SpanContext} object associated with this Span. + * Sets the status of the span. When an error is provided, it will be recorded on the span as well. * - * Get an immutable, serializable identifier for this span that can be used - * to create new child spans. Returned SpanContext is usable even after the - * span ends. - * - * @returns the SpanContext object associated with this Span. - */ - spanContext(): SpanContext; - /** - * Sets an attribute to the span. - * - * Sets a single Attribute with the key and value passed as arguments. - * - * @param key - the key for this attribute. - * @param value - the value for this attribute. Setting a value null or - * undefined is invalid and will result in undefined behavior. - */ - setAttribute(key: string, value: SpanAttributeValue): this; - /** - * Sets attributes to the span. - * - * @param attributes - the attributes that will be added. - * null or undefined attribute values - * are invalid and will result in undefined behavior. - */ - setAttributes(attributes: SpanAttributes): this; - /** - * Adds an event to the Span. - * - * @param name - the name of the event. - * @param attributesOrStartTime - the attributes that will be added; these are - * associated with this event. Can be also a start time - * if type is TimeInput and 3rd param is undefined - * @param startTime - start time of the event. - */ - addEvent( - name: string, - attributesOrStartTime?: SpanAttributes | TimeInput, - startTime?: TimeInput - ): this; - /** - * Sets a status to the span. If used, this will override the default Span - * status. Default is {@link SpanStatusCode.UNSET}. SetStatus overrides the value - * of previous calls to SetStatus on the Span. - * - * @param status - the SpanStatus to set. - */ - setStatus(status: SpanStatus): this; - /** - * Marks the end of Span execution. - * - * Call to End of a Span MUST not have any effects on child spans. Those may - * still be running and can be ended later. - * - * Do not return `this`. The Span generally should not be used after it - * is ended so chaining is not desired in this context. - * - * @param endTime - the time to set as Span's end time. If not provided, - * use the current time as the span's end time. + * @param status - The {@link SpanStatus} to set on the span. */ - end(endTime?: TimeInput): void; + setStatus(status: SpanStatus): void; + /** - * Returns the flag whether this span will be recorded. + * Sets a given attribute on a span. * - * @returns true if this Span is active and recording information like events - * with the `AddEvent` operation and attributes using `setAttributes`. + * @param name - The attribute's name. + * @param value - The attribute's value to set. May be any non-nullish value. */ - isRecording(): boolean; + setAttribute(name: string, value: unknown): void; /** - * Sets exception as a span event - * @param exception - the exception the only accepted values are string or Error - * @param time - the time to set as Span's event time. If not provided, - * use the current time. + * Ends the span. */ - recordException(exception: Exception, time?: TimeInput): void; + end(): void; /** - * Updates the Span name. + * Records an exception on a {@link TracingSpan} without modifying its status. * - * This will override the name provided via {@link Tracer.startSpan}. + * When recording an unhandled exception that should fail the span, please use {@link TracingSpan.setStatus} instead. * - * Upon this update, any sampling behavior based on Span name will depend on - * the implementation. + * @param exception - The exception to record on the span. * - * @param name - the Span name. */ - updateName(name: string): this; -} - -/** - * Shorthand enum for common traceFlags values inside SpanContext - */ -export const enum TraceFlags { - /** No flag set. */ - NONE = 0x0, - /** Caller is collecting trace information. */ - SAMPLED = 0x1, -} + recordException(exception: Error | string): void; -/** - * A light interface that tries to be structurally compatible with OpenTelemetry - */ -export interface SpanContext { /** - * UUID of a trace. - */ - traceId: string; - /** - * UUID of a Span. - */ - spanId: string; - /** - * https://www.w3.org/TR/trace-context/#trace-flags - */ - traceFlags: number; - /** - * Tracing-system-specific info to propagate. - * - * The tracestate field value is a `list` as defined below. The `list` is a - * series of `list-members` separated by commas `,`, and a list-member is a - * key/value pair separated by an equals sign `=`. Spaces and horizontal tabs - * surrounding `list-members` are ignored. There can be a maximum of 32 - * `list-members` in a `list`. - * More Info: https://www.w3.org/TR/trace-context/#tracestate-field + * Returns true if this {@link TracingSpan} is recording information. * - * Examples: - * Single tracing system (generic format): - * tracestate: rojo=00f067aa0ba902b7 - * Multiple tracing systems (with different formatting): - * tracestate: rojo=00f067aa0ba902b7,congo=t61rcWkgMzE + * Depending on the span implementation, this may return false if the span is not being sampled. */ - traceState?: TraceState; -} - -/** - * Used to specify a span that is linked to another. - */ -export interface Link { - /** The {@link SpanContext} of a linked span. */ - context: SpanContext; - - /** A set of {@link SpanAttributes} on the link. */ - attributes?: SpanAttributes; + isRecording(): boolean; } -/** - * Attributes for a Span. - */ -export interface SpanAttributes { +/** An immutable context bag of tracing values for the current operation. */ +export interface TracingContext { /** - * Attributes for a Span. - */ - [attributeKey: string]: SpanAttributeValue | undefined; -} -/** - * Attribute values may be any non-nullish primitive value except an object. - * - * null or undefined attribute values are invalid and will result in undefined behavior. - */ -export declare type SpanAttributeValue = - | string - | number - | boolean - | Array - | Array - | Array; - -/** - * An interface that enables manual propagation of Spans - */ -export interface SpanOptions { - /** - * Attributes to set on the Span + * Sets a given object on a context. + * @param key - The key of the given context value. + * @param value - The value to set on the context. + * + * @returns - A new context with the given value set. */ - attributes?: SpanAttributes; - - /** {@link Link}s span to other spans */ - links?: Link[]; - + setValue(key: symbol, value: unknown): TracingContext; /** - * The type of Span. Default to SpanKind.INTERNAL + * Gets an object from the context if it exists. + * @param key - The key of the given context value. + * + * @returns - The value of the given context value if it exists, otherwise `undefined`. */ - kind?: SpanKind; - + getValue(key: symbol): unknown; /** - * A manually specified start time for the created `Span` object. + * Deletes an object from the context if it exists. + * @param key - The key of the given context value to delete. */ - startTime?: TimeInput; + deleteValue(key: symbol): TracingContext; } /** * Tracing options to set on an operation. */ export interface OperationTracingOptions { - /** - * OpenTelemetry context to use for created Spans. - */ - tracingContext?: Context; -} - -/** - * OpenTelemetry compatible interface for Context - */ -export interface Context { - /** - * Get a value from the context. - * - * @param key - key which identifies a context value - */ - getValue(key: symbol): unknown; - /** - * Create a new context which inherits from this context and has - * the given key set to the given value. - * - * @param key - context key for which to set the value - * @param value - value to set for the given key - */ - setValue(key: symbol, value: unknown): Context; - /** - * Return a new context which inherits from this context but does - * not contain a value for the given key. - * - * @param key - context key for which to clear a value - */ - deleteValue(key: symbol): Context; + /** The context to use for created Tracing Spans. */ + tracingContext?: TracingContext; } diff --git a/sdk/core/core-tracing/src/tracingClient.ts b/sdk/core/core-tracing/src/tracingClient.ts new file mode 100644 index 000000000000..fa39b03d9214 --- /dev/null +++ b/sdk/core/core-tracing/src/tracingClient.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + OperationTracingOptions, + TracingClient, + TracingClientOptions, + TracingContext, + TracingSpan, + TracingSpanOptions, +} from "./interfaces"; +import { getInstrumenter } from "./instrumenter"; +import { knownContextKeys } from "./tracingContext"; + +/** + * Creates a new tracing client. + * + * @param options - Options used to configure the tracing client. + * @returns - An instance of {@link TracingClient}. + */ +export function createTracingClient(options: TracingClientOptions): TracingClient { + const { namespace, packageName, packageVersion } = options; + + function startSpan( + name: string, + operationOptions?: Options, + spanOptions?: TracingSpanOptions + ): { + span: TracingSpan; + updatedOptions: Options; + } { + const startSpanResult = getInstrumenter().startSpan(name, { + ...spanOptions, + packageName: packageName, + packageVersion: packageVersion, + tracingContext: operationOptions?.tracingOptions?.tracingContext, + }); + let tracingContext = startSpanResult.tracingContext; + const span = startSpanResult.span; + if (!tracingContext.getValue(knownContextKeys.namespace)) { + tracingContext = tracingContext.setValue(knownContextKeys.namespace, namespace); + } + span.setAttribute("az.namespace", tracingContext.getValue(knownContextKeys.namespace)); + const updatedOptions = { + ...operationOptions, + tracingOptions: { + tracingContext: tracingContext, + }, + } as Options; + return { + span, + updatedOptions, + }; + } + + async function withSpan< + Options extends { tracingOptions?: { tracingContext?: TracingContext } }, + Callback extends ( + updatedOptions: Options, + span: Omit + ) => ReturnType + >( + name: string, + operationOptions: Options, + callback: Callback, + spanOptions?: TracingSpanOptions + ): Promise> { + const { span, updatedOptions } = startSpan(name, operationOptions, spanOptions); + try { + const result = await withContext(updatedOptions.tracingOptions!.tracingContext!, () => + Promise.resolve(callback(updatedOptions, span)) + ); + span.setStatus({ status: "success" }); + return result; + } catch (err) { + span.setStatus({ status: "error", error: err }); + throw err; + } finally { + span.end(); + } + } + + function withContext< + CallbackArgs extends unknown[], + Callback extends (...args: CallbackArgs) => ReturnType + >( + context: TracingContext, + callback: Callback, + ...callbackArgs: CallbackArgs + ): ReturnType { + return getInstrumenter().withContext(context, callback, ...callbackArgs); + } + + /** + * Parses a traceparent header value into a span identifier. + * + * @param traceparentHeader - The traceparent header to parse. + * @returns An implementation-specific identifier for the span. + */ + function parseTraceparentHeader(traceparentHeader: string): TracingContext | undefined { + return getInstrumenter().parseTraceparentHeader(traceparentHeader); + } + + /** + * Creates a set of request headers to propagate tracing information to a backend. + * + * @param tracingContext - The context containing the span to serialize. + * @returns The set of headers to add to a request. + */ + function createRequestHeaders(tracingContext?: TracingContext): Record { + return getInstrumenter().createRequestHeaders(tracingContext); + } + + return { + startSpan, + withSpan, + withContext, + parseTraceparentHeader, + createRequestHeaders, + }; +} diff --git a/sdk/core/core-tracing/src/tracingContext.ts b/sdk/core/core-tracing/src/tracingContext.ts new file mode 100644 index 000000000000..7c0d6369561e --- /dev/null +++ b/sdk/core/core-tracing/src/tracingContext.ts @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { TracingClient, TracingContext, TracingSpan } from "./interfaces"; + +/** @internal */ +export const knownContextKeys = { + span: Symbol.for("@azure/core-tracing span"), + namespace: Symbol.for("@azure/core-tracing namespace"), + client: Symbol.for("@azure/core-tracing client"), + parentContext: Symbol.for("@azure/core-tracing parent context"), +}; + +/** + * Creates a new {@link TracingContext} with the given options. + * @param options - A set of known keys that may be set on the context. + * @returns A new {@link TracingContext} with the given options. + * + * @internal + */ +export function createTracingContext(options: CreateTracingContextOptions = {}): TracingContext { + let context: TracingContext = new TracingContextImpl(options.parentContext); + if (options.span) { + context = context.setValue(knownContextKeys.span, options.span); + } + if (options.client) { + context = context.setValue(knownContextKeys.client, options.client); + } + if (options.namespace) { + context = context.setValue(knownContextKeys.namespace, options.namespace); + } + return context; +} + +/** @internal */ +export class TracingContextImpl implements TracingContext { + private _contextMap: Map; + constructor(initialContext?: TracingContext) { + this._contextMap = + initialContext instanceof TracingContextImpl + ? new Map(initialContext._contextMap) + : new Map(); + } + + setValue(key: symbol, value: unknown): TracingContext { + const newContext = new TracingContextImpl(this); + newContext._contextMap.set(key, value); + return newContext; + } + + getValue(key: symbol): unknown { + return this._contextMap.get(key); + } + + deleteValue(key: symbol): TracingContext { + const newContext = new TracingContextImpl(this); + newContext._contextMap.delete(key); + return newContext; + } +} + +/** + * Represents a set of items that can be set when creating a new {@link TracingContext}. + */ +export interface CreateTracingContextOptions { + /** The {@link parentContext} - the newly created context will contain all the values of the parent context unless overriden. */ + parentContext?: TracingContext; + /** An initial span to set on the context. */ + span?: TracingSpan; + /** The tracing client used to create this context. */ + client?: TracingClient; + /** The namespace to set on any child spans. */ + namespace?: string; +} diff --git a/sdk/core/core-tracing/src/utils/traceParentHeader.ts b/sdk/core/core-tracing/src/utils/traceParentHeader.ts deleted file mode 100644 index 67b394e4d157..000000000000 --- a/sdk/core/core-tracing/src/utils/traceParentHeader.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { SpanContext, TraceFlags } from "../interfaces"; - -const VERSION = "00"; - -/** - * Generates a `SpanContext` given a `traceparent` header value. - * @param traceParent - Serialized span context data as a `traceparent` header value. - * @returns The `SpanContext` generated from the `traceparent` value. - */ -export function extractSpanContextFromTraceParentHeader( - traceParentHeader: string -): SpanContext | undefined { - const parts = traceParentHeader.split("-"); - - if (parts.length !== 4) { - return; - } - - const [version, traceId, spanId, traceOptions] = parts; - - if (version !== VERSION) { - return; - } - - const traceFlags = parseInt(traceOptions, 16); - - const spanContext: SpanContext = { - spanId, - traceId, - traceFlags, - }; - - return spanContext; -} - -/** - * Generates a `traceparent` value given a span context. - * @param spanContext - Contains context for a specific span. - * @returns The `spanContext` represented as a `traceparent` value. - */ -export function getTraceParentHeader(spanContext: SpanContext): string | undefined { - const missingFields: string[] = []; - if (!spanContext.traceId) { - missingFields.push("traceId"); - } - if (!spanContext.spanId) { - missingFields.push("spanId"); - } - - if (missingFields.length) { - return; - } - - const flags = spanContext.traceFlags || TraceFlags.NONE; - const hexFlags = flags.toString(16); - const traceFlags = hexFlags.length === 1 ? `0${hexFlags}` : hexFlags; - - // https://www.w3.org/TR/trace-context/#traceparent-header-field-values - return `${VERSION}-${spanContext.traceId}-${spanContext.spanId}-${traceFlags}`; -} diff --git a/sdk/core/core-tracing/test/createSpan.spec.ts b/sdk/core/core-tracing/test/createSpan.spec.ts deleted file mode 100644 index a9843d2a92d1..000000000000 --- a/sdk/core/core-tracing/test/createSpan.spec.ts +++ /dev/null @@ -1,269 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { - Context, - SpanKind, - getSpanContext, - context as otContext, - setSpan, -} from "../src/interfaces"; -import { createSpanFunction, isTracingDisabled, knownSpanAttributes } from "../src/createSpan"; -import { OperationTracingOptions } from "../src/interfaces"; -import { TestSpan } from "./util/testSpan"; -import { TestTracerProvider } from "./util/testTracerProvider"; -import { assert } from "chai"; - -describe("createSpan", () => { - let createSpan: ReturnType; - let tracerProvider: TestTracerProvider; - - beforeEach(() => { - tracerProvider = new TestTracerProvider(); - tracerProvider.register(); - createSpan = createSpanFunction({ namespace: "Microsoft.Test", packagePrefix: "Azure.Test" }); - }); - - afterEach(() => { - tracerProvider.disable(); - }); - - it("is backwards compatible at runtime with versions prior to preview.13", () => { - const testSpan = tracerProvider.getTracer("test").startSpan("test"); - const someContext = setSpan(otContext.active(), testSpan); - - // Ensure we are backwards compatible with { tracingOptions: { spanOptions } } shape which was - // used prior to preview.13 for setting span options. - const options = { - tracingOptions: { - tracingContext: someContext, - spanOptions: { - kind: SpanKind.CLIENT, - attributes: { - foo: "bar", - }, - }, - }, - }; - - const expectedSpanOptions = { - kind: SpanKind.CLIENT, - attributes: { - foo: "bar", - "az.namespace": "Microsoft.Test", - }, - }; - - const { span, updatedOptions } = <{ span: TestSpan; updatedOptions: any }>( - createSpan("testMethod", options) - ); - assert.deepEqual(updatedOptions.tracingOptions.spanOptions, expectedSpanOptions); - assert.equal(span.kind, SpanKind.CLIENT); - assert.equal(span.attributes.foo, "bar"); - - assert.equal( - updatedOptions.tracingOptions.tracingContext.getValue( - knownSpanAttributes.AZ_NAMESPACE.contextKey - ), - "Microsoft.Test" - ); - }); - - it("returns a created span with the right metadata", () => { - const testSpan = tracerProvider.getTracer("test").startSpan("testing"); - - const someContext = setSpan(otContext.active(), testSpan); - - const { span, updatedOptions } = <{ span: TestSpan; updatedOptions: any }>createSpan( - "testMethod", - { - // validate that we dumbly just copy any fields (this makes future upgrades easier) - someOtherField: "someOtherFieldValue", - tracingOptions: { - // validate that we dumbly just copy any fields (this makes future upgrades easier) - someOtherField: "someOtherFieldValue", - tracingContext: someContext, - }, - }, - { kind: SpanKind.SERVER } - ); - assert.strictEqual(span.name, "Azure.Test.testMethod"); - assert.equal(span.attributes["az.namespace"], "Microsoft.Test"); - - assert.equal(updatedOptions.someOtherField, "someOtherFieldValue"); - assert.equal(updatedOptions.tracingOptions.someOtherField, "someOtherFieldValue"); - - assert.equal(span.kind, SpanKind.SERVER); - assert.equal( - updatedOptions.tracingOptions.tracingContext.getValue(Symbol.for("az.namespace")), - "Microsoft.Test" - ); - }); - - it("preserves existing attributes", () => { - const testSpan = tracerProvider.getTracer("test").startSpan("testing"); - - const someContext = setSpan(otContext.active(), testSpan).setValue( - Symbol.for("someOtherKey"), - "someOtherValue" - ); - - const { span, updatedOptions } = <{ span: TestSpan; updatedOptions: any }>( - createSpan("testMethod", { - someTopLevelField: "someTopLevelFieldValue", - tracingOptions: { - someOtherTracingField: "someOtherTracingValue", - tracingContext: someContext, - }, - }) - ); - assert.strictEqual(span.name, "Azure.Test.testMethod"); - assert.equal(span.attributes["az.namespace"], "Microsoft.Test"); - - assert.equal( - updatedOptions.tracingOptions.tracingContext.getValue(Symbol.for("someOtherKey")), - "someOtherValue" - ); - assert.equal(updatedOptions.someTopLevelField, "someTopLevelFieldValue"); - assert.equal(updatedOptions.tracingOptions.someOtherTracingField, "someOtherTracingValue"); - }); - - it("namespace and packagePrefix can be empty (and thus ignored)", () => { - const cf = createSpanFunction({ - namespace: "", - packagePrefix: "", - }); - - const { span, updatedOptions } = cf("myVerbatimOperationName", {} as any, { - attributes: { - testAttribute: "testValue", - }, - }); - - assert.equal( - (span as TestSpan).name, - "myVerbatimOperationName", - "Expected name to not change because there is no packagePrefix." - ); - assert.notExists( - (span as TestSpan).attributes["az.namespace"], - "Expected az.namespace not to be set because there is no namespace" - ); - - assert.notExists( - updatedOptions.tracingOptions.tracingContext?.getValue(Symbol.for("az.namespace")) - ); - }); - - it("createSpans, testing parent/child relationship", () => { - const createSpanFn = createSpanFunction({ - namespace: "Microsoft.Test", - packagePrefix: "Azure.Test", - }); - - let parentContext: Context; - - // create the parent span and do some basic checks. - { - const op: { tracingOptions: OperationTracingOptions } = { - tracingOptions: {}, - }; - - const { span, updatedOptions } = createSpanFn("parent", op); - assert.ok(span); - - parentContext = updatedOptions.tracingOptions!.tracingContext!; - - assert.ok(parentContext); - assert.notDeepEqual(parentContext, otContext.active(), "new child context should be created"); - assert.equal( - getSpanContext(parentContext!)?.spanId, - span.spanContext().spanId, - "context returned in the updated options should point to our newly created span" - ); - } - - const { span: childSpan, updatedOptions } = createSpanFn("child", { - tracingOptions: { - tracingContext: parentContext, - }, - }); - assert.ok(childSpan); - - assert.ok(updatedOptions.tracingOptions.tracingContext); - assert.equal( - getSpanContext(updatedOptions.tracingOptions.tracingContext!)?.spanId, - childSpan.spanContext().spanId - ); - }); - - it("is robust when no options are passed in", () => { - const { span, updatedOptions } = <{ span: TestSpan; updatedOptions: any }>createSpan("foo"); - assert.exists(span); - assert.exists(updatedOptions); - assert.exists(updatedOptions.tracingOptions.spanOptions); - assert.exists(updatedOptions.tracingOptions.tracingContext); - }); - - it("returns a no-op tracer if AZURE_TRACING_DISABLED is set", function (this: Mocha.Context) { - if (typeof process === "undefined") { - this.skip(); - } - process.env.AZURE_TRACING_DISABLED = "true"; - - const testSpan = tracerProvider.getTracer("test").startSpan("testing"); - - const someContext = setSpan(otContext.active(), testSpan); - - const { span } = <{ span: TestSpan; updatedOptions: any }>createSpan("testMethod", { - tracingOptions: { - // validate that we dumbly just copy any fields (this makes future upgrades easier) - someOtherField: "someOtherFieldValue", - tracingContext: someContext, - spanOptions: { - kind: SpanKind.SERVER, - }, - } as OperationTracingOptions as any, - }); - assert.isFalse(span.isRecording()); - delete process.env.AZURE_TRACING_DISABLED; - }); - - describe("IsTracingDisabled", () => { - beforeEach(function (this: Mocha.Context) { - if (typeof process === "undefined") { - this.skip(); - } - }); - it("is false when env var is blank or missing", () => { - process.env.AZURE_TRACING_DISABLED = ""; - assert.isFalse(isTracingDisabled()); - delete process.env.AZURE_TRACING_DISABLED; - assert.isFalse(isTracingDisabled()); - }); - - it("is false when env var is 'false'", () => { - process.env.AZURE_TRACING_DISABLED = "false"; - assert.isFalse(isTracingDisabled()); - process.env.AZURE_TRACING_DISABLED = "False"; - assert.isFalse(isTracingDisabled()); - process.env.AZURE_TRACING_DISABLED = "FALSE"; - assert.isFalse(isTracingDisabled()); - delete process.env.AZURE_TRACING_DISABLED; - }); - - it("is false when env var is 0", () => { - process.env.AZURE_TRACING_DISABLED = "0"; - assert.isFalse(isTracingDisabled()); - delete process.env.AZURE_TRACING_DISABLED; - }); - - it("is true otherwise", () => { - process.env.AZURE_TRACING_DISABLED = "true"; - assert.isTrue(isTracingDisabled()); - process.env.AZURE_TRACING_DISABLED = "1"; - assert.isTrue(isTracingDisabled()); - delete process.env.AZURE_TRACING_DISABLED; - }); - }); -}); diff --git a/sdk/core/core-tracing/test/instrumenter.spec.ts b/sdk/core/core-tracing/test/instrumenter.spec.ts new file mode 100644 index 000000000000..43ca92d749a1 --- /dev/null +++ b/sdk/core/core-tracing/test/instrumenter.spec.ts @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { Instrumenter, TracingSpan } from "../src/interfaces"; +import { + createDefaultInstrumenter, + createDefaultTracingSpan, + getInstrumenter, + useInstrumenter, +} from "../src/instrumenter"; +import { createTracingContext, knownContextKeys } from "../src/tracingContext"; + +describe("Instrumenter", () => { + describe("NoOpInstrumenter", () => { + let instrumenter: Instrumenter; + const name = "test-operation"; + + beforeEach(() => { + instrumenter = createDefaultInstrumenter(); + }); + + describe("#startSpan", () => { + const packageName = "test-package"; + + it("returns a new context", () => { + const { tracingContext } = instrumenter.startSpan(name, { packageName }); + assert.exists(tracingContext); + }); + + it("returns context with all existing properties", () => { + const [key, value] = [Symbol.for("key"), "value"]; + const context = createTracingContext().setValue(key, value); + + const { tracingContext } = instrumenter.startSpan(name, { + tracingContext: context, + packageName, + }); + assert.strictEqual(tracingContext.getValue(key), value); + }); + }); + + describe("#withContext", () => { + it("applies the callback", () => { + const expectedText = "expected"; + const result = instrumenter.withContext(createTracingContext(), () => expectedText); + assert.equal(result, expectedText); + }); + }); + + describe("#parseTraceparentHeader", () => { + it("returns undefined", () => { + assert.isUndefined(instrumenter.parseTraceparentHeader("")); + }); + }); + + describe("#createRequestHeaders", () => { + it("returns an empty object", () => { + assert.isEmpty(instrumenter.createRequestHeaders(createTracingContext())); + assert.isEmpty( + instrumenter.createRequestHeaders( + createTracingContext().setValue(knownContextKeys.span, createDefaultTracingSpan()) + ) + ); + }); + }); + }); + + describe("NoOpSpan", () => { + it("supports all TracingSpan methods", () => { + const span: TracingSpan = createDefaultTracingSpan(); + span.setStatus({ status: "success" }); + span.setAttribute("foo", "bar"); + span.recordException(new Error("test")); + span.end(); + assert.isFalse(span.isRecording()); + }); + }); + + describe("useInstrumenter", () => { + it("allows setting and getting a global instrumenter", () => { + const instrumenter = getInstrumenter(); + assert.exists(instrumenter); + + const newInstrumenter = createDefaultInstrumenter(); + useInstrumenter(newInstrumenter); + assert.strictEqual(getInstrumenter(), newInstrumenter); + }); + }); +}); diff --git a/sdk/core/core-tracing/test/interfaces.spec.ts b/sdk/core/core-tracing/test/interfaces.spec.ts index 1284c397fdd9..5e8318326f7b 100644 --- a/sdk/core/core-tracing/test/interfaces.spec.ts +++ b/sdk/core/core-tracing/test/interfaces.spec.ts @@ -1,86 +1,21 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -import * as coreAuth from "@azure/core-auth"; -import * as coreTracing from "../src/interfaces"; -import * as openTelemetry from "@opentelemetry/api"; -import { TestTracer } from "./util/testTracer"; import { assert } from "chai"; -import { getTracer } from "../src/interfaces"; - -type coreAuthTracingOptions = Required["tracingOptions"]; - -describe("interface compatibility", () => { - it("SpanContext is assignable", () => { - const context: coreTracing.SpanContext = { - spanId: "", - traceId: "", - traceFlags: coreTracing.TraceFlags.NONE, - }; - - const OTContext: openTelemetry.SpanContext = context; - const context2: coreTracing.SpanContext = OTContext; - - assert.ok(context2); - }); - - it("SpanOptions can be passed to OT", () => { - const spanOptions: coreTracing.SpanOptions = { - attributes: { - hello: "world", - }, - kind: coreTracing.SpanKind.INTERNAL, - links: [ - { - context: { - traceFlags: coreTracing.TraceFlags.NONE, - spanId: "", - traceId: "", - }, - }, - ], - }; - - const oTSpanOptions: openTelemetry.SpanOptions = spanOptions; - assert.ok(oTSpanOptions); - }); - - it("core-auth", () => { - const coreTracingOptions: Required = { - tracingContext: coreTracing.context.active(), - }; - - const t: Required< - Omit< - coreAuthTracingOptions, - keyof Required | "spanOptions" - > - > = {}; - assert.ok(t, "core-tracing and core-auth should have the same properties"); - - const t2: Required< - Omit< - coreTracing.OperationTracingOptions, - keyof Required | "spanOptions" - > - > = {}; - assert.ok(t2, "core-tracing and core-auth should have the same properties"); - - const authTracingOptions: coreAuth.GetTokenOptions["tracingOptions"] = coreTracingOptions; - assert.ok(authTracingOptions); - }); - - describe("getTracer", () => { - it("returns a tracer with a given name and version", () => { - const tracer = getTracer("test", "1.0.0") as TestTracer; - assert.equal(tracer.name, "test"); - assert.equal(tracer.version, "1.0.0"); - }); +import { createTracingContext } from "../src/tracingContext"; +import * as coreTracing from "../src"; +import * as coreAuth from "@azure/core-auth"; - it("returns a tracer with a default name no version if not provided", () => { - const tracer = getTracer() as TestTracer; - assert.isNotEmpty(tracer.name); - assert.isUndefined(tracer.version); +describe("Interface compatibility", () => { + describe("OperationTracingOptions", () => { + it("is compatible with core-auth", () => { + const tracingOptions: coreTracing.OperationTracingOptions = { + tracingContext: createTracingContext({}), + }; + const authOptions: coreAuth.GetTokenOptions = { + tracingOptions, + }; + assert.ok(authOptions.tracingOptions); }); }); }); diff --git a/sdk/core/core-tracing/test/traceParentHeader.spec.ts b/sdk/core/core-tracing/test/traceParentHeader.spec.ts deleted file mode 100644 index 2a1ccd26d213..000000000000 --- a/sdk/core/core-tracing/test/traceParentHeader.spec.ts +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { SpanContext, TraceFlags } from "@opentelemetry/api"; -import { extractSpanContextFromTraceParentHeader, getTraceParentHeader } from "../src"; -import { assert } from "chai"; - -describe("traceParentHeader", () => { - describe("#extractSpanContextFromTraceParentHeader", () => { - it("should extract a SpanContext from a properly formatted traceparent", () => { - const traceId = "11111111111111111111111111111111"; - const spanId = "2222222222222222"; - const flags = "00"; - const traceParentHeader = `00-${traceId}-${spanId}-${flags}`; - - const spanContext = extractSpanContextFromTraceParentHeader(traceParentHeader); - if (!spanContext) { - assert.fail("Extracted spanContext should be defined."); - return; - } - assert.equal(spanContext.traceId, traceId, "Extracted traceId does not match expectation."); - assert.equal(spanContext.spanId, spanId, "Extracted spanId does not match expectation."); - assert.equal( - spanContext.traceFlags, - TraceFlags.NONE, - "Extracted traceFlags do not match expectations." - ); - }); - - describe("should return undefined", () => { - it("when traceparent contains an unknown version", () => { - const traceId = "11111111111111111111111111111111"; - const spanId = "2222222222222222"; - const flags = "00"; - const traceParentHeader = `99-${traceId}-${spanId}-${flags}`; - - const spanContext = extractSpanContextFromTraceParentHeader(traceParentHeader); - - assert.strictEqual( - spanContext, - undefined, - "Invalid traceparent version should return undefined spanContext." - ); - }); - - it("when traceparent is malformed", () => { - const traceParentHeader = `123abc`; - - const spanContext = extractSpanContextFromTraceParentHeader(traceParentHeader); - - assert.strictEqual( - spanContext, - undefined, - "Malformed traceparent should return undefined spanContext." - ); - }); - }); - }); - - describe("#getTraceParentHeader", () => { - it("should return a traceparent header from a SpanContext", () => { - const spanContext: SpanContext = { - spanId: "2222222222222222", - traceId: "11111111111111111111111111111111", - traceFlags: TraceFlags.SAMPLED, - }; - - const traceParentHeader = getTraceParentHeader(spanContext); - - assert.strictEqual( - traceParentHeader, - `00-${spanContext.traceId}-${spanContext.spanId}-01`, - "TraceParentHeader does not match expectation." - ); - }); - - it("should set the traceFlag to UNSAMPLED if not provided in SpanContext", () => { - const spanContext: SpanContext = { - spanId: "2222222222222222", - traceId: "11111111111111111111111111111111", - traceFlags: TraceFlags.NONE, - }; - - const traceParentHeader = getTraceParentHeader(spanContext); - - assert.strictEqual( - traceParentHeader, - `00-${spanContext.traceId}-${spanContext.spanId}-00`, - "TraceParentHeader does not match expectation." - ); - }); - - describe("should return undefined", () => { - it("when traceId is not defined", () => { - const spanContext: any = { - spanId: "2222222222222222", - traceFlags: TraceFlags.SAMPLED, - }; - - const traceParentHeader = getTraceParentHeader(spanContext); - - assert.strictEqual( - traceParentHeader, - undefined, - "Missing traceId should return undefined spanContext." - ); - }); - - it("when spanId is not defined", () => { - const spanContext: any = { - traceId: "11111111111111111111111111111111", - traceFlags: TraceFlags.SAMPLED, - }; - - const traceParentHeader = getTraceParentHeader(spanContext); - - assert.strictEqual( - traceParentHeader, - undefined, - "Missing spanId should return undefined spanContext." - ); - }); - }); - }); -}); diff --git a/sdk/core/core-tracing/test/tracingClient.spec.ts b/sdk/core/core-tracing/test/tracingClient.spec.ts new file mode 100644 index 000000000000..1cb13421fbac --- /dev/null +++ b/sdk/core/core-tracing/test/tracingClient.spec.ts @@ -0,0 +1,197 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import sinon from "sinon"; +import { Instrumenter, TracingClient, TracingContext, TracingSpan } from "../src/interfaces"; +import { + createDefaultInstrumenter, + createDefaultTracingSpan, + useInstrumenter, +} from "../src/instrumenter"; +import { createTracingClient } from "../src/tracingClient"; +import { createTracingContext, knownContextKeys } from "../src/tracingContext"; + +describe("TracingClient", () => { + let instrumenter: Instrumenter; + let span: TracingSpan; + let context: TracingContext; + let client: TracingClient; + const expectedNamespace = "Microsoft.Test"; + + beforeEach(() => { + instrumenter = createDefaultInstrumenter(); + span = createDefaultTracingSpan(); + context = createTracingContext(); + + useInstrumenter(instrumenter); + client = createTracingClient({ + namespace: expectedNamespace, + packageName: "test-package", + packageVersion: "1.0.0", + }); + }); + + afterEach(() => { + sinon.restore(); + }); + + describe("#startSpan", () => { + it("sets namespace on span", () => { + // Set our instrumenter to always return the same span and context so we + // can inspect them. + instrumenter.startSpan = () => { + return { + span, + tracingContext: context, + }; + }; + const setAttributeSpy = sinon.spy(span, "setAttribute"); + client.startSpan("test", {}); + assert.isTrue( + setAttributeSpy.calledWith("az.namespace", expectedNamespace), + `expected span.setAttribute("az.namespace", "${expectedNamespace}") to have been called` + ); + }); + + it("passes package information to instrumenter", () => { + const instrumenterStartSpanSpy = sinon.spy(instrumenter, "startSpan"); + client.startSpan("test", {}); + assert.isTrue(instrumenterStartSpanSpy.called); + const args = instrumenterStartSpanSpy.getCall(0).args; + + assert.equal(args[0], "test"); + assert.equal(args[1]?.packageName, "test-package"); + assert.equal(args[1]?.packageVersion, "1.0.0"); + }); + + it("sets namespace on context", () => { + const { updatedOptions } = client.startSpan("test"); + assert.equal( + updatedOptions.tracingOptions?.tracingContext?.getValue(knownContextKeys.namespace), + expectedNamespace + ); + }); + + it("does not override existing namespace on context", () => { + context = createTracingContext().setValue(knownContextKeys.namespace, "Existing.Namespace"); + const { updatedOptions } = client.startSpan("test", { + tracingOptions: { tracingContext: context }, + }); + assert.equal( + updatedOptions.tracingOptions?.tracingContext?.getValue(knownContextKeys.namespace), + "Existing.Namespace" + ); + }); + + it("Returns tracingContext in updatedOptions", () => { + let { updatedOptions } = client.startSpan("test"); + assert.exists(updatedOptions.tracingOptions?.tracingContext); + updatedOptions = client.startSpan("test", updatedOptions).updatedOptions; + assert.exists(updatedOptions.tracingOptions?.tracingContext); + }); + }); + + describe("#withSpan", () => { + const spanName = "test-span"; + + it("sets namespace on span", async () => { + // Set our instrumenter to always return the same span and context so we + // can inspect them. + instrumenter.startSpan = () => { + return { + span, + tracingContext: context, + }; + }; + const setAttributeSpy = sinon.spy(span, "setAttribute"); + await client.withSpan(spanName, {}, async () => { + // no op + }); + assert.isTrue( + setAttributeSpy.calledWith("az.namespace", expectedNamespace), + `expected span.setAttribute("az.namespace", "${expectedNamespace}") to have been called` + ); + }); + + it("passes options and span to callback", async () => { + await client.withSpan(spanName, { foo: "foo", bar: "bar" } as any, (options, currentSpan) => { + assert.exists(currentSpan); + assert.exists(options); + assert.equal(options.foo, "foo"); + assert.equal(options.bar, "bar"); + return true; + }); + }); + + it("promisifies synchronous functions", async () => { + const result = await client.withSpan(spanName, {}, () => { + return 5; + }); + assert.equal(result, 5); + }); + + it("supports asynchronous functions", async () => { + const result = await client.withSpan(spanName, {}, () => { + return Promise.resolve(5); + }); + assert.equal(result, 5); + }); + + it("returns context with all existing properties", async () => { + const [key, value] = [Symbol.for("key"), "value"]; + const parentContext = createTracingContext().setValue(key, value); + await client.withSpan( + spanName, + { + tracingOptions: { + tracingContext: parentContext, + }, + }, + (updatedOptions) => { + assert.strictEqual(updatedOptions.tracingOptions.tracingContext.getValue(key), value); + } + ); + }); + + describe("with a successful callback", () => { + it("sets status on the span", async () => { + // Set our instrumenter to always return the same span and context so we + // can inspect them. + instrumenter.startSpan = () => { + return { + span, + tracingContext: context, + }; + }; + const setStatusSpy = sinon.spy(span, "setStatus"); + await client.withSpan(spanName, {}, () => Promise.resolve(42)); + + assert.isTrue(setStatusSpy.calledWith(sinon.match({ status: "success" }))); + }); + }); + + describe("with an error", () => { + it("sets status on the span", async () => { + // Set our instrumenter to always return the same span and context so we + // can inspect them. + instrumenter.startSpan = () => { + return { + span, + tracingContext: context, + }; + }; + const setStatusSpy = sinon.spy(span, "setStatus"); + let errorThrown = false; + try { + await client.withSpan(spanName, {}, () => Promise.reject(new Error("test"))); + } catch (err) { + errorThrown = true; + assert.isTrue(setStatusSpy.calledWith(sinon.match({ status: "error", error: err }))); + } + + assert.isTrue(errorThrown); + }); + }); + }); +}); diff --git a/sdk/core/core-tracing/test/tracingContext.spec.ts b/sdk/core/core-tracing/test/tracingContext.spec.ts new file mode 100644 index 000000000000..b09087b27fb1 --- /dev/null +++ b/sdk/core/core-tracing/test/tracingContext.spec.ts @@ -0,0 +1,121 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { createDefaultTracingSpan } from "../src/instrumenter"; +import { createTracingClient } from "../src/tracingClient"; +import { TracingContextImpl, createTracingContext, knownContextKeys } from "../src/tracingContext"; + +describe("TracingContext", () => { + describe("TracingContextImpl", () => { + let context: TracingContextImpl; + + beforeEach(() => { + context = new TracingContextImpl(); + }); + + it("can be created from an existing context map", () => { + const existingContext = createTracingContext() + .setValue(Symbol.for("key1"), "value1") + .setValue(Symbol.for("key2"), "value2"); + const newContext = new TracingContextImpl(existingContext); + assert.equal(newContext.getValue(Symbol.for("key1")), "value1"); + assert.equal(newContext.getValue(Symbol.for("key2")), "value2"); + }); + + describe("getValue and setValue", () => { + it("returns new context with new value", () => { + const newContext = context.setValue(Symbol.for("newKey"), "newVal"); + assert.equal(newContext.getValue(Symbol.for("newKey")), "newVal"); + }); + + it("returns new context with all existing values", () => { + const newContext = context + .setValue(Symbol.for("newKey"), "newVal") + .setValue(Symbol.for("someOtherKey"), "someOtherVal") + .setValue(Symbol.for("lastKey"), "lastVal"); + + // inherited context data should remain + assert.equal(newContext.getValue(Symbol.for("newKey")), "newVal"); + }); + + it("does not modify existing context", () => { + context.setValue(Symbol.for("newKey"), "newVal"); + assert.notExists(context.getValue(Symbol.for("newKey"))); + }); + + it("can fetch parent chain data", () => { + const newContext = context + .setValue(Symbol.for("ancestry"), "grandparent") + .setValue(Symbol.for("ancestry"), "parent") + .setValue(Symbol.for("self"), "self"); // use a different key for current context + + assert.equal(newContext.getValue(Symbol.for("ancestry")), "parent"); + assert.equal(newContext.getValue(Symbol.for("self")), "self"); + }); + }); + + describe("#deleteValue", () => { + it("returns new context without deleted value", () => { + const newContext = context + .setValue(Symbol.for("newKey"), "newVal") + .deleteValue(Symbol.for("newKey")); + assert.notExists(newContext.getValue(Symbol.for("newKey"))); + }); + + it("does not modify existing context", () => { + const newContext = context.setValue(Symbol.for("newKey"), "newVal"); + newContext.deleteValue(Symbol.for("newKey")); + assert.equal(newContext.getValue(Symbol.for("newKey")), "newVal"); + }); + + it("deletes parent chain data", () => { + const newContext = context + .setValue(Symbol.for("ancestry"), "grandparent") + .setValue(Symbol.for("ancestry"), "parent") + .setValue(Symbol.for("self"), "self"); + + assert.isDefined(newContext.getValue(Symbol.for("ancestry"))); + assert.isDefined(newContext.getValue(Symbol.for("self"))); + + const updatedContext = newContext + .deleteValue(Symbol.for("ancestry")) + .deleteValue(Symbol.for("self")); + + assert.isUndefined(updatedContext.getValue(Symbol.for("ancestry"))); + assert.isUndefined(updatedContext.getValue(Symbol.for("self"))); + }); + }); + }); + + describe("#createTracingContext", () => { + it("returns a new context", () => { + const context = createTracingContext(); + assert.exists(context); + assert.instanceOf(context, TracingContextImpl); + }); + + it("can add known attributes", () => { + const client = createTracingClient({ namespace: "test", packageName: "test" }); + const span = createDefaultTracingSpan(); + const namespace = "test-namespace"; + const newContext = createTracingContext({ + client, + span, + namespace, + }); + assert.strictEqual(newContext.getValue(knownContextKeys.client), client); + assert.strictEqual(newContext.getValue(knownContextKeys.namespace), namespace); + assert.strictEqual(newContext.getValue(knownContextKeys.span), span); + }); + + it("can be initialized from an existing context", () => { + const parentContext = createTracingContext().setValue( + knownContextKeys.namespace, + "test-namespace" + ); + const newContext = createTracingContext({ parentContext: parentContext }); + assert.equal(newContext.getValue(knownContextKeys.namespace), "test-namespace"); + }); + }); +}); diff --git a/sdk/core/core-tracing/test/util/testTracerProvider.ts b/sdk/core/core-tracing/test/util/testTracerProvider.ts deleted file mode 100644 index 5d50cbff13f1..000000000000 --- a/sdk/core/core-tracing/test/util/testTracerProvider.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -import { Tracer, TracerProvider, trace } from "@opentelemetry/api"; -import { TestTracer } from "./testTracer"; - -export class TestTracerProvider implements TracerProvider { - private tracerCache: Map = new Map(); - - getTracer(name: string, version?: string): Tracer { - const tracerKey = `${name}${version}`; - if (!this.tracerCache.has(tracerKey)) { - this.tracerCache.set(tracerKey, new TestTracer(name, version)); - } - return this.tracerCache.get(tracerKey)!; - } - - register(): boolean { - return trace.setGlobalTracerProvider(this); - } - - disable(): void { - trace.disable(); - } -} diff --git a/sdk/instrumentation/ci.yml b/sdk/instrumentation/ci.yml new file mode 100644 index 000000000000..887d18613705 --- /dev/null +++ b/sdk/instrumentation/ci.yml @@ -0,0 +1,30 @@ +# NOTE: Please refer to https://aka.ms/azsdk/engsys/ci-yaml before editing this file. + +trigger: + branches: + include: + - main + - release/* + - hotfix/* + paths: + include: + - sdk/instrumentation/ + +pr: + branches: + include: + - main + - feature/* + - release/* + - hotfix/* + paths: + include: + - sdk/instrumentation/ + +extends: + template: ../../eng/pipelines/templates/stages/archetype-sdk-client.yml + parameters: + ServiceDirectory: instrumentation + Artifacts: + - name: opentelemetry-instrumentation-azure-sdk + safeName: opentelemetryinstrumentationazuresdk diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/.nycrc b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/.nycrc new file mode 100644 index 000000000000..320eddfeffb9 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/.nycrc @@ -0,0 +1,19 @@ +{ + "include": [ + "dist-esm/src/**/*.js" + ], + "exclude": [ + "**/*.d.ts", + "dist-esm/src/generated/*" + ], + "reporter": [ + "text-summary", + "html", + "cobertura" + ], + "exclude-after-remap": false, + "sourceMap": true, + "produce-source-map": true, + "instrument": true, + "all": true + } diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/CHANGELOG.md b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/CHANGELOG.md new file mode 100644 index 000000000000..3940e21b480c --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/CHANGELOG.md @@ -0,0 +1,13 @@ +# Release History + +## 1.0.0-beta.1 (Unreleased) + +### Features Added + +This marks the first beta release of the OpenTelemetry Instrumentation library for the Azure SDK which will enable OpenTelemetry Span creation for Azure SDK client libraries. + +### Breaking Changes + +### Bugs Fixed + +### Other Changes diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/LICENSE b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/LICENSE new file mode 100644 index 000000000000..ea8fb1516028 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/README.md b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/README.md new file mode 100644 index 000000000000..0b9bf89d1a38 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/README.md @@ -0,0 +1,94 @@ +# Azure OpenTelemetry Instrumentation library for JavaScript + +## Getting started + +### Currently supported environments + +- [LTS versions of Node.js](https://nodejs.org/about/releases/) +- Latest versions of Safari, Chrome, Edge, and Firefox. + +See our [support policy](https://github.com/Azure/azure-sdk-for-js/blob/main/SUPPORT.md) for more details. + +### Prerequisites + +- An [Azure subscription][azure_sub]. +- The [@opentelemetry/instrumentation][otel_instrumentation] package. + +You'll need to configure the OpenTelemetry SDK in order to produce Telemetry data. While configuring OpenTelemetry is outside the scope of this README, we encourage you to review the [OpenTelemetry documentation][otel_documentation] in order to get started using OpenTelemetry. + +### Install the `@azure/opentelemetry-instrumentation-azure-sdk` package + +Install the Azure OpenTelemetry Instrumentation client library with `npm`: + +```bash +npm install @azure/opentelemetry-instrumentation-azure-sdk +``` + +### Browser support + +#### JavaScript Bundle + +To use this client library in the browser, first you need to use a bundler. For details on how to do this, please refer to our [bundling documentation](https://aka.ms/AzureSDKBundling). + +## Key concepts + +- The **createAzureSdkInstrumentation** function is the main hook exported by this library which provides a way to create an Azure SDK Instrumentation object to be registered with OpenTelemetry. + +### Compatibility with existing Client Libraries + +- TODO, we should describe what versions of core-tracing are compatible here... + +## Examples + +### Enable OpenTelemetry instrumentation + +```javascript +const { registerInstrumentations } = require("@opentelemetry/instrumentation"); +const { createAzureSdkInstrumentation } = require("@azure/opentelemetry-instrumentation-azure-sdk"); + +// Configure exporters, tracer providers, etc. +// Please refer to the OpenTelemetry documentation for more information. + +registerInstrumentations({ + instrumentations: [createAzureSdkInstrumentation()], +}); + +// Continue to import any Azure SDK client libraries after registering the instrumentation. + +const { keyClient } = require("@azure/keyvault-keys"); + +// Do something cool with the keyClient... +``` + +## Troubleshooting + +### Logging + +Enabling logging may help uncover useful information about failures. In order to see a log of HTTP requests and responses, set the `AZURE_LOG_LEVEL` environment variable to `info`. Alternatively, logging can be enabled at runtime by calling `setLogLevel` in the `@azure/logger`: + +```javascript +import { setLogLevel } from "@azure/logger"; + +setLogLevel("info"); +``` + +For more detailed instructions on how to enable logs, you can look at the [@azure/logger package docs](https://github.com/Azure/azure-sdk-for-js/tree/main/sdk/core/logger). + +## Next steps + +- TODO: no samples yet, so the link verification fails. Add link to samples... + +## Contributing + +If you'd like to contribute to this library, please read the [contributing guide](https://github.com/Azure/azure-sdk-for-js/blob/main/CONTRIBUTING.md) to learn more about how to build and test the code. + +## Related projects + +- [Microsoft Azure SDK for Javascript](https://github.com/Azure/azure-sdk-for-js) + +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-js%2Fsdk%2Ftemplate%2Ftemplate%2FREADME.png) + +[azure_cli]: https://docs.microsoft.com/cli/azure +[azure_sub]: https://azure.microsoft.com/free/ +[otel_instrumentation]: https://www.npmjs.com/package/@opentelemetry/instrumentation +[otel_documentation]: https://opentelemetry.io/docs/js/ diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/api-extractor.json b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/api-extractor.json new file mode 100644 index 000000000000..db769b85026a --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/api-extractor.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "types/src/index.d.ts", + "docModel": { + "enabled": true + }, + "apiReport": { + "enabled": true, + "reportFolder": "./review" + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "", + "publicTrimmedFilePath": "./types/latest/opentelemetry-instrumentation-azure-sdk.d.ts" + }, + "messages": { + "tsdocMessageReporting": { + "default": { + "logLevel": "none" + } + }, + "extractorMessageReporting": { + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-unresolved-link": { + "logLevel": "none" + } + } + } +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/karma.conf.js b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/karma.conf.js new file mode 100644 index 000000000000..3156ad1ba1f5 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/karma.conf.js @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +// https://github.com/karma-runner/karma-chrome-launcher +process.env.CHROME_BIN = require("puppeteer").executablePath(); +require("dotenv").config(); +const { + jsonRecordingFilterFunction, + isPlaybackMode, + isSoftRecordMode, + isRecordMode, +} = require("@azure-tools/test-recorder"); + +module.exports = function (config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: "./", + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ["mocha"], + + plugins: [ + "karma-mocha", + "karma-mocha-reporter", + "karma-chrome-launcher", + "karma-edge-launcher", + "karma-firefox-launcher", + "karma-ie-launcher", + "karma-env-preprocessor", + "karma-coverage", + "karma-junit-reporter", + "karma-json-to-file-reporter", + "karma-json-preprocessor", + ], + + // list of files / patterns to load in the browser + files: [ + "dist-test/index.browser.js", + { pattern: "dist-test/index.browser.js.map", type: "html", included: false, served: true }, + ].concat(isPlaybackMode() || isSoftRecordMode() ? ["recordings/browsers/**/*.json"] : []), + + // list of files / patterns to exclude + exclude: [], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + "**/*.js": ["env"], + "recordings/browsers/**/*.json": ["json"], + // IMPORTANT: COMMENT following line if you want to debug in your browsers!! + // Preprocess source file to calculate code coverage, however this will make source file unreadable + //"dist-test/index.browser.js": ["coverage"] + }, + + envPreprocessor: ["TEST_MODE", "AZURE_CLIENT_ID", "AZURE_CLIENT_SECRET", "AZURE_TENANT_ID"], + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ["mocha", "coverage", "junit", "json-to-file"], + + coverageReporter: { + // specify a common output directory + dir: "coverage-browser/", + reporters: [{ type: "json", subdir: ".", file: "coverage.json" }], + }, + + junitReporter: { + outputDir: "", // results will be saved as $outputDir/$browserName.xml + outputFile: "test-results.browser.xml", // if included, results will be saved as $outputDir/$browserName/$outputFile + suite: "", // suite will become the package name attribute in xml testsuite element + useBrowserName: false, // add browser name to report and classes names + nameFormatter: undefined, // function (browser, result) to customize the name attribute in xml testcase element + classNameFormatter: undefined, // function (browser, result) to customize the classname attribute in xml testcase element + properties: {}, // key value pair of properties to add to the section of the report + }, + + jsonToFileReporter: { + filter: jsonRecordingFilterFunction, + outputPath: ".", + }, + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // --no-sandbox allows our tests to run in Linux without having to change the system. + // --disable-web-security allows us to authenticate from the browser without having to write tests using interactive auth, which would be far more complex. + browsers: ["ChromeHeadlessNoSandbox"], + customLaunchers: { + ChromeHeadlessNoSandbox: { + base: "ChromeHeadless", + flags: ["--no-sandbox", "--disable-web-security"], + }, + }, + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: 1, + + browserNoActivityTimeout: 600000, + browserDisconnectTimeout: 10000, + browserDisconnectTolerance: 3, + browserConsoleLogOptions: { + terminal: !isRecordMode(), + }, + + client: { + mocha: { + // change Karma's debug.html to the mocha web reporter + reporter: "html", + timeout: "600000", + }, + }, + }); +}; diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/package.json b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/package.json new file mode 100644 index 000000000000..c1cb0f43e845 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/package.json @@ -0,0 +1,135 @@ +{ + "name": "@azure/opentelemetry-instrumentation-azure-sdk", + "version": "1.0.0-beta.1", + "description": "Instrumentation client for the Azure SDK.", + "sdk-type": "client", + "main": "dist/index.js", + "module": "dist-esm/src/index.js", + "browser": { + "./dist-esm/src/instrumentation.js": "./dist-esm/src/instrumentation.browser.js" + }, + "//metadata": { + "constantPaths": [ + { + "path": "src/constants.ts", + "prefix": "SDK_VERSION" + } + ] + }, + "types": "types/latest/opentelemetry-instrumentation-azure-sdk.d.ts", + "typesVersions": { + "<3.6": { + "*": [ + "types/3.1/opentelemetry-instrumentation-azure-sdk.d.ts" + ] + } + }, + "scripts": { + "audit": "node ../../../common/scripts/rush-audit.js && rimraf node_modules package-lock.json && npm i --package-lock-only 2>&1 && npm audit", + "build:samples": "echo Obsolete", + "build:test": "tsc -p . && rollup -c 2>&1", + "build:types": "downlevel-dts types/latest/ types/3.1/", + "build": "npm run clean && tsc -p . && rollup -c 2>&1 && api-extractor run --local && npm run build:types", + "check-format": "prettier --list-different --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json}\"", + "clean": "rimraf dist dist-* temp types *.tgz *.log", + "docs": "typedoc --excludePrivate --excludeNotExported --excludeExternals --stripInternal --mode file --out ./dist/docs ./src", + "execute:samples": "dev-tool samples run samples-dev", + "extract-api": "tsc -p . && api-extractor run --local", + "format": "prettier --write --config ../../../.prettierrc.json --ignore-path ../../../.prettierignore \"src/**/*.ts\" \"test/**/*.ts\" \"samples-dev/**/*.ts\" \"*.{js,json}\"", + "generate:client": "autorest --typescript ./swagger/README.md", + "integration-test:browser": "karma start --single-run", + "integration-test:node": "nyc mocha -r esm --require source-map-support/register --reporter ../../../common/tools/mocha-multi-reporter.js --timeout 5000000 --full-trace \"dist-esm/test/{,!(browser)/**/}/*.spec.js\"", + "integration-test": "npm run integration-test:node && npm run integration-test:browser", + "lint:fix": "eslint package.json api-extractor.json src test --ext .ts --fix --fix-type [problem,suggestion]", + "lint": "eslint package.json api-extractor.json src test --ext .ts", + "pack": "npm pack 2>&1", + "test:browser": "npm run clean && npm run build:test && npm run unit-test:browser && npm run integration-test:browser", + "test:node": "npm run clean && tsc -p . && npm run unit-test:node && npm run integration-test:node", + "test": "npm run clean && tsc -p . && npm run unit-test:node && rollup -c 2>&1 && npm run unit-test:browser && npm run integration-test", + "unit-test:browser": "karma start --single-run", + "unit-test:node": "mocha -r esm -r ts-node/register --reporter ../../../common/tools/mocha-multi-reporter.js --timeout 1200000 --full-trace --exclude \"test/**/browser/*.spec.ts\" \"test/**/*.spec.ts\"", + "unit-test": "npm run unit-test:node && npm run unit-test:browser" + }, + "files": [ + "dist/", + "dist-esm/src/", + "types/latest/", + "types/3.1/", + "README.md", + "LICENSE" + ], + "repository": "github:Azure/azure-sdk-for-js", + "engines": { + "node": ">=12.0.0" + }, + "keywords": [ + "azure", + "cloud", + "tracing", + "typescript" + ], + "author": "Microsoft Corporation", + "license": "MIT", + "bugs": { + "url": "https://github.com/Azure/azure-sdk-for-js/issues" + }, + "homepage": "https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/README.md", + "sideEffects": false, + "prettier": "@azure/eslint-plugin-azure-sdk/prettier.json", + "dependencies": { + "@azure/core-tracing": "1.0.0-preview.14", + "@azure/logger": "^1.0.0", + "@opentelemetry/api": "^1.0.3", + "@opentelemetry/core": "^1.0.1", + "@opentelemetry/instrumentation": "^0.27.0", + "tslib": "^2.2.0" + }, + "devDependencies": { + "@azure-tools/test-recorder": "^1.0.0", + "@azure/dev-tool": "^1.0.0", + "@azure/eslint-plugin-azure-sdk": "^3.0.0", + "@microsoft/api-extractor": "^7.18.11", + "@types/chai": "^4.1.6", + "@types/mocha": "^7.0.2", + "@types/node": "^12.0.0", + "@types/sinon": "^10.0.6", + "chai": "^4.2.0", + "cross-env": "^7.0.2", + "dotenv": "^8.2.0", + "downlevel-dts": "~0.4.0", + "eslint": "^7.15.0", + "esm": "^3.2.18", + "inherits": "^2.0.3", + "karma": "^6.2.0", + "karma-chrome-launcher": "^3.0.0", + "karma-coverage": "^2.0.0", + "karma-edge-launcher": "^0.4.2", + "karma-env-preprocessor": "^0.1.1", + "karma-firefox-launcher": "^1.1.0", + "karma-ie-launcher": "^1.0.0", + "karma-json-preprocessor": "^0.3.3", + "karma-json-to-file-reporter": "^1.0.1", + "karma-junit-reporter": "^2.0.1", + "karma-mocha": "^2.0.1", + "karma-mocha-reporter": "^2.2.5", + "mocha": "^7.1.1", + "mocha-junit-reporter": "^1.18.0", + "nyc": "^14.0.0", + "prettier": "^2.5.1", + "rimraf": "^3.0.0", + "rollup": "^1.16.3", + "sinon": "^12.0.1", + "source-map-support": "^0.5.9", + "typedoc": "0.15.2", + "typescript": "~4.2.0", + "util": "^0.12.1" + }, + "//sampleConfiguration": { + "skipFolder": true, + "disableDocsMs": true, + "productName": "Azure OpenTelemetry Instrumentation", + "productSlugs": [], + "apiRefLink": "https://docs.microsoft.com/javascript/api/", + "requiredResources": {} + } +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/review/opentelemetry-instrumentation-azure-sdk.api.md b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/review/opentelemetry-instrumentation-azure-sdk.api.md new file mode 100644 index 000000000000..6c381f39eed2 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/review/opentelemetry-instrumentation-azure-sdk.api.md @@ -0,0 +1,23 @@ +## API Report File for "@azure/opentelemetry-instrumentation-azure-sdk" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +import { AzureLogger } from '@azure/logger'; +import { Instrumentation } from '@opentelemetry/instrumentation'; +import { InstrumentationConfig } from '@opentelemetry/instrumentation'; + +// @public +export interface AzureSdkInstrumentationOptions extends InstrumentationConfig { +} + +// @public +export function createAzureSdkInstrumentation(options?: AzureSdkInstrumentationOptions): Instrumentation; + +// @public +export const logger: AzureLogger; + +// (No @packageDocumentation comment for this package) + +``` diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/rollup.config.js b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/rollup.config.js new file mode 100644 index 000000000000..5d7deee44c14 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/rollup.config.js @@ -0,0 +1,3 @@ +import { makeConfig } from "@azure/dev-tool/shared-config/rollup"; + +export default makeConfig(require("./package.json")); diff --git a/sdk/core/core-tracing/src/utils/browser.d.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/constants.ts similarity index 53% rename from sdk/core/core-tracing/src/utils/browser.d.ts rename to sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/constants.ts index 72f5b34bc8cc..47dc16dd0f7c 100644 --- a/sdk/core/core-tracing/src/utils/browser.d.ts +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/constants.ts @@ -1,5 +1,4 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -interface Window {} -declare let self: Window & typeof globalThis; +export const SDK_VERSION: string = "1.0.0-beta.1"; diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/index.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/index.ts new file mode 100644 index 000000000000..c184a0d70b4c --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/index.ts @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export * from "./logger"; +export * from "./instrumentation"; diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.browser.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.browser.ts new file mode 100644 index 000000000000..b74fedddb12d --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.browser.ts @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + Instrumentation, + InstrumentationBase, + InstrumentationConfig, +} from "@opentelemetry/instrumentation"; +import { SDK_VERSION } from "./constants"; +import { useInstrumenter } from "@azure/core-tracing"; +import { OpenTelemetryInstrumenter } from "./instrumenter"; + +/** + * Configuration options that can be passed to {@link createAzureSdkInstrumentation} function. + */ +export interface AzureSdkInstrumentationOptions extends InstrumentationConfig {} + +/** + * The instrumentation module for the Azure SDK. Implements OpenTelemetry's {@link Instrumentation}. + */ +class AzureSdkInstrumentation extends InstrumentationBase { + constructor(options: AzureSdkInstrumentationOptions = {}) { + super( + "@azure/opentelemetry-instrumentation-azure-sdk", + SDK_VERSION, + Object.assign({}, options) + ); + } + /** In the browser we rely on overriding the `enable` function instead as there are no modules to patch. */ + protected init(): void { + // no-op + } + + /** + * Entrypoint for the module registration. Ensures the global instrumenter is set to use OpenTelemetry. + */ + enable(): void { + useInstrumenter(new OpenTelemetryInstrumenter()); + } + + disable(): void { + // no-op + } +} + +/** + * Enables Azure SDK Instrumentation using OpenTelemetry for Azure SDK client libraries. + * + * When registerd, any Azure data plane package will begin emitting tracing spans for internal calls + * as well as network calls + * + * Example usage: + * ```ts + * const openTelemetryInstrumentation = require("@opentelemetry/instrumentation"); + * openTelemetryInstrumentation.registerInstrumentations({ + * instrumentations: [createAzureSdkInstrumentation()], + * }) + * ``` + */ +export function createAzureSdkInstrumentation( + options: AzureSdkInstrumentationOptions = {} +): Instrumentation { + return new AzureSdkInstrumentation(options); +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.ts new file mode 100644 index 000000000000..086460049529 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumentation.ts @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + Instrumentation, + InstrumentationBase, + InstrumentationConfig, + InstrumentationModuleDefinition, + InstrumentationNodeModuleDefinition, +} from "@opentelemetry/instrumentation"; +import type * as coreTracing from "@azure/core-tracing"; +import { OpenTelemetryInstrumenter } from "./instrumenter"; +import { SDK_VERSION } from "./constants"; + +/** + * Configuration options that can be passed to {@link createAzureSdkInstrumentation} function. + */ +export interface AzureSdkInstrumentationOptions extends InstrumentationConfig {} + +/** + * The instrumentation module for the Azure SDK. Implements OpenTelemetry's {@link Instrumentation}. + */ +class AzureSdkInstrumentation extends InstrumentationBase { + constructor(options: AzureSdkInstrumentationOptions = {}) { + super( + "@azure/opentelemetry-instrumentation-azure-sdk", + SDK_VERSION, + Object.assign({}, options) + ); + } + /** + * Entrypoint for the module registration. + * + * @returns The patched \@azure/core-tracing module after setting its instrumenter. + */ + protected init(): + | void + | InstrumentationModuleDefinition + | InstrumentationModuleDefinition[] { + const result: InstrumentationModuleDefinition = + new InstrumentationNodeModuleDefinition( + "@azure/core-tracing", + ["^1.0.0-preview.14", "^1.0.0"], + (moduleExports) => { + if (typeof moduleExports.useInstrumenter === "function") { + moduleExports.useInstrumenter(new OpenTelemetryInstrumenter()); + } + + return moduleExports; + } + ); + // Needed to support 1.0.0-preview.14 + result.includePrerelease = true; + return result; + } +} + +/** + * Enables Azure SDK Instrumentation using OpenTelemetry for Azure SDK client libraries. + * + * When registerd, any Azure data plane package will begin emitting tracing spans for internal calls + * as well as network calls + * + * Example usage: + * ```ts + * const openTelemetryInstrumentation = require("@opentelemetry/instrumentation"); + * openTelemetryInstrumentation.registerInstrumentations({ + * instrumentations: [createAzureSdkInstrumentation()], + * }) + * ``` + * + * @remarks + * + * As OpenTelemetry instrumentations rely on patching required modules, you should register + * this instrumentation as early as possible and before loading any Azure Client Libraries. + */ +export function createAzureSdkInstrumentation( + options: AzureSdkInstrumentationOptions = {} +): Instrumentation { + return new AzureSdkInstrumentation(options); +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts new file mode 100644 index 000000000000..8cd86717849d --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/instrumenter.ts @@ -0,0 +1,65 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + Instrumenter, + InstrumenterSpanOptions, + TracingContext, + TracingSpan, +} from "@azure/core-tracing"; + +import { trace, context, defaultTextMapGetter, defaultTextMapSetter } from "@opentelemetry/api"; +import { W3CTraceContextPropagator } from "@opentelemetry/core"; +import { OpenTelemetrySpanWrapper } from "./spanWrapper"; + +import { toSpanOptions } from "./transformations"; + +// While default propagation is user-configurable, Azure services always use the W3C implementation. +export const propagator = new W3CTraceContextPropagator(); + +export class OpenTelemetryInstrumenter implements Instrumenter { + startSpan( + name: string, + spanOptions: InstrumenterSpanOptions + ): { span: TracingSpan; tracingContext: TracingContext } { + const span = trace + .getTracer(spanOptions.packageName, spanOptions.packageVersion) + .startSpan(name, toSpanOptions(spanOptions)); + + const ctx = spanOptions?.tracingContext || context.active(); + + return { + span: new OpenTelemetrySpanWrapper(span), + tracingContext: trace.setSpan(ctx, span), + }; + } + withContext< + CallbackArgs extends unknown[], + Callback extends (...args: CallbackArgs) => ReturnType + >( + tracingContext: TracingContext, + callback: Callback, + ...callbackArgs: CallbackArgs + ): ReturnType { + return context.with( + tracingContext, + callback, + /** Assume caller will bind `this` or use arrow functions */ undefined, + ...callbackArgs + ); + } + + parseTraceparentHeader(traceparentHeader: string): TracingContext { + return propagator.extract( + context.active(), + { traceparent: traceparentHeader }, + defaultTextMapGetter + ); + } + + createRequestHeaders(tracingContext?: TracingContext): Record { + const headers: Record = {}; + propagator.inject(tracingContext || context.active(), headers, defaultTextMapSetter); + return headers; + } +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/logger.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/logger.ts new file mode 100644 index 000000000000..73fd29e1fd26 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/logger.ts @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { createClientLogger } from "@azure/logger"; + +/** + * The \@azure/logger configuration for this package. + */ +export const logger = createClientLogger("opentelemetry-instrumentation-azure-sdk"); diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/spanWrapper.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/spanWrapper.ts new file mode 100644 index 000000000000..3555f83e9356 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/spanWrapper.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { SpanStatus, TracingSpan } from "@azure/core-tracing"; +import { Span, SpanStatusCode, SpanAttributeValue } from "@opentelemetry/api"; + +export class OpenTelemetrySpanWrapper implements TracingSpan { + private _span: Span; + + constructor(span: Span) { + this._span = span; + } + + setStatus(status: SpanStatus): void { + if (status.status === "error") { + if (status.error) { + this._span.setStatus({ code: SpanStatusCode.ERROR, message: status.error.toString() }); + this.recordException(status.error); + } else { + this._span.setStatus({ code: SpanStatusCode.ERROR }); + } + } else if (status.status === "success") { + this._span.setStatus({ code: SpanStatusCode.OK }); + } + } + + setAttribute(name: string, value: unknown): void { + if (value !== null && value !== undefined) { + this._span.setAttribute(name, value as SpanAttributeValue); + } + } + + end(): void { + this._span.end(); + } + + recordException(exception: string | Error): void { + this._span.recordException(exception); + } + + isRecording(): boolean { + return this._span.isRecording(); + } + + /** + * Allows getting the wrapped span as needed. + * @internal + * + * @returns The underlying span + */ + unwrap(): Span { + return this._span; + } +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/transformations.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/transformations.ts new file mode 100644 index 000000000000..2bc9ad8926c0 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/src/transformations.ts @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { InstrumenterSpanOptions, TracingSpanKind, TracingSpanLink } from "@azure/core-tracing"; +import { + Link, + SpanAttributeValue, + SpanAttributes, + SpanKind, + SpanOptions, + trace, +} from "@opentelemetry/api"; + +/** + * Converts our TracingSpanKind to the corresponding OpenTelemetry SpanKind. + * + * By default it will return {@link SpanKind.INTERNAL} + * @param tracingSpanKind - The core tracing {@link TracingSpanKind} + * @returns - The OpenTelemetry {@link SpanKind} + */ +export function toOpenTelemetrySpanKind( + tracingSpanKind?: K +): SpanKindMapping[K] { + const key = (tracingSpanKind || "internal").toUpperCase() as keyof typeof SpanKind; + return SpanKind[key] as SpanKindMapping[K]; +} + +/** + * A mapping between our {@link TracingSpanKind} union type and OpenTelemetry's {@link SpanKind}. + */ +type SpanKindMapping = { + client: SpanKind.CLIENT; + server: SpanKind.SERVER; + producer: SpanKind.PRODUCER; + consumer: SpanKind.CONSUMER; + internal: SpanKind.INTERNAL; +}; + +/** + * Converts core-tracing's TracingSpanLink to OpenTelemetry's Link + * + * @param spanLinks - The core tracing {@link TracingSpanLink} to convert + * @returns A set of {@link Link}s + */ +function toOpenTelemetryLinks(spanLinks: TracingSpanLink[] = []): Link[] { + return spanLinks.reduce((acc, tracingSpanLink) => { + const spanContext = trace.getSpanContext(tracingSpanLink.tracingContext); + if (spanContext) { + acc.push({ + context: spanContext, + attributes: toOpenTelemetrySpanAttributes(tracingSpanLink.attributes), + }); + } + return acc; + }, [] as Link[]); +} + +/** + * Converts core-tracing's span attributes to OpenTelemetry attributes. + * + * @param spanAttributes - The set of attributes to convert. + * @returns An {@link SpanAttributes} to set on a span. + */ +function toOpenTelemetrySpanAttributes( + spanAttributes: { [key: string]: unknown } | undefined +): SpanAttributes { + const attributes: ReturnType = {}; + for (const key in spanAttributes) { + // Any non-nullish value is allowed. + if (spanAttributes[key] !== null && spanAttributes[key] !== undefined) { + attributes[key] = spanAttributes[key] as SpanAttributeValue; + } + } + return attributes; +} + +/** + * Converts core-tracing span options to OpenTelemetry options. + * + * @param spanOptions - The {@link InstrumenterSpanOptions} to convert. + * @returns An OpenTelemetry {@link SpanOptions} that can be used when creating a span. + */ +export function toSpanOptions(spanOptions?: InstrumenterSpanOptions): SpanOptions { + const { spanAttributes, spanLinks, spanKind } = spanOptions || {}; + + const attributes: SpanAttributes = toOpenTelemetrySpanAttributes(spanAttributes); + const kind = toOpenTelemetrySpanKind(spanKind); + const links = toOpenTelemetryLinks(spanLinks); + + return { + attributes, + kind, + links, + }; +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/instrumenter.spec.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/instrumenter.spec.ts new file mode 100644 index 000000000000..0fc493c5d5df --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/instrumenter.spec.ts @@ -0,0 +1,244 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { OpenTelemetryInstrumenter, propagator } from "../../src/instrumenter"; +import { trace, context, SpanKind } from "@opentelemetry/api"; +import { TracingSpan, TracingSpanKind } from "@azure/core-tracing"; +import { TestSpan } from "./util/testSpan"; +import { TestTracer } from "./util/testTracer"; +import { resetTracer, setTracer } from "./util/testTracerProvider"; +import sinon from "sinon"; +import { Context } from "mocha"; +import { OpenTelemetrySpanWrapper } from "../../src/spanWrapper"; + +function unwrap(span: TracingSpan): TestSpan { + return (span as OpenTelemetrySpanWrapper).unwrap() as TestSpan; +} + +describe("OpenTelemetryInstrumenter", () => { + const instrumenter = new OpenTelemetryInstrumenter(); + + describe("#createRequestHeaders", () => { + afterEach(() => { + sinon.restore(); + }); + + it("uses the passed in context if it exists", () => { + let propagationSpy = sinon.spy(propagator); + const span = new TestTracer().startSpan("test"); + let tracingContext = trace.setSpan(context.active(), span); + instrumenter.createRequestHeaders(tracingContext); + assert.isTrue(propagationSpy.inject.calledWith(tracingContext)); + }); + + it("uses the active context if no context was provided", () => { + let propagationSpy = sinon.spy(propagator); + instrumenter.createRequestHeaders(); + const activeContext = context.active(); + assert.isTrue(propagationSpy.inject.calledWith(activeContext)); + }); + }); + + // TODO: the following still uses existing test support for OTel. + // Once the new APIs are available we should move away from those. + describe("#startSpan", () => { + let tracer: TestTracer; + const packageName = "test-package"; + const packageVersion = "test-version"; + beforeEach(() => { + tracer = setTracer(tracer); + }); + + afterEach(() => { + resetTracer(); + }); + + it("returns a newly started TracingSpan", () => { + const { span } = instrumenter.startSpan("test", { packageName, packageVersion }); + const otSpan = unwrap(span); + assert.equal(otSpan, tracer.getActiveSpans()[0]); + assert.equal(otSpan.kind, SpanKind.INTERNAL); + }); + + it("passes package information to the tracer", () => { + const getTracerSpy = sinon.spy(trace, "getTracer"); + instrumenter.startSpan("test", { packageName, packageVersion }); + + assert.isTrue(getTracerSpy.calledWith(packageName, packageVersion)); + }); + + describe("with an existing context", () => { + it("returns a context that contains all existing fields", () => { + const currentContext = context.active().setValue(Symbol.for("foo"), "bar"); + + const { tracingContext } = instrumenter.startSpan("test", { + tracingContext: currentContext, + packageName, + }); + + assert.equal(tracingContext.getValue(Symbol.for("foo")), "bar"); + }); + + it("sets span on the context", () => { + const currentContext = context.active().setValue(Symbol.for("foo"), "bar"); + + const { span, tracingContext } = instrumenter.startSpan("test", { + tracingContext: currentContext, + packageName, + }); + + assert.equal(trace.getSpan(tracingContext), unwrap(span)); + }); + }); + + describe("when a context is not provided", () => { + it("uses the active context", () => { + const contextSpy = sinon.spy(context, "active"); + + instrumenter.startSpan("test", { packageName, packageVersion }); + + assert.isTrue(contextSpy.called); + }); + + it("sets span on the context", () => { + const { span, tracingContext } = instrumenter.startSpan("test", { + packageName, + packageVersion, + }); + + assert.equal(trace.getSpan(tracingContext), unwrap(span)); + }); + }); + + describe("spanOptions", () => { + it("passes attributes to started span", () => { + const spanAttributes = { + attr1: "val1", + attr2: "val2", + }; + const { span } = instrumenter.startSpan("test", { + spanAttributes, + packageName, + packageVersion, + }); + + assert.deepEqual(unwrap(span).attributes, spanAttributes); + }); + + describe("spanKind", () => { + it("maps spanKind correctly", () => { + const { span } = instrumenter.startSpan("test", { + packageName, + spanKind: "client", + }); + assert.equal(unwrap(span).kind, SpanKind.CLIENT); + }); + + it("defaults spanKind to INTERNAL if omitted", () => { + const { span } = instrumenter.startSpan("test", { packageName }); + assert.equal(unwrap(span).kind, SpanKind.INTERNAL); + }); + + // TODO: what's the right behavior? throw? log and continue? + it("defaults spanKind to INTERNAL if an invalid spanKind is provided", () => { + const { span } = instrumenter.startSpan("test", { + packageName, + spanKind: "foo" as TracingSpanKind, + }); + assert.equal(unwrap(span).kind, SpanKind.INTERNAL); + }); + }); + + it("supports spanLinks", () => { + const { tracingContext: linkedSpanContext } = instrumenter.startSpan("linked", { + packageName, + }); + + const { span } = instrumenter.startSpan("test", { + packageName, + spanLinks: [ + { + tracingContext: linkedSpanContext, + attributes: { + attr1: "value1", + }, + }, + ], + }); + + const links = unwrap(span).links; + assert.equal(links.length, 1); + assert.deepEqual(links[0].attributes, { attr1: "value1" }); + assert.deepEqual(links[0].context, trace.getSpan(linkedSpanContext)?.spanContext()); + }); + + it("supports spanLinks from traceparentHeader", () => { + const linkedContext = instrumenter.parseTraceparentHeader( + "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01" + ); + + const { span } = instrumenter.startSpan("test", { + packageName, + spanLinks: [{ tracingContext: linkedContext! }], + }); + + const links = unwrap(span).links; + assert.equal(links.length, 1); + assert.deepEqual(links[0].context, trace.getSpan(linkedContext!)?.spanContext()); + }); + }); + }); + + describe("#withContext", () => { + it("passes the correct arguments to OpenTelemetry", function (this: Context) { + const contextSpy = sinon.spy(context, "with"); + // eslint-disable-next-line @typescript-eslint/explicit-function-return-type + const callback = (arg1: number) => arg1 + 42; + const callbackArg = 37; + const activeContext = context.active(); + instrumenter.withContext(activeContext, callback, callbackArg); + + assert.isTrue(contextSpy.calledWith(activeContext, callback, undefined, callbackArg)); + }); + + it("works when caller binds `this`", function (this: Context) { + // a bit of a silly test but demonstrates how to bind `this` correctly + // and ensures the behavior does not regress + + // Function syntax + instrumenter.withContext(context.active(), function (this: any) { + assert.isUndefined(this); + }); + instrumenter.withContext( + context.active(), + function (this: any) { + assert.equal(this, 42); + }.bind(42) + ); + + // Arrow syntax + // eslint-disable-next-line @typescript-eslint/no-this-alias + const that = this; + instrumenter.withContext(context.active(), () => { + assert.equal(this, that); + }); + }); + + it("Returns the value of the callback", () => { + const result = instrumenter.withContext(context.active(), () => 42); + assert.equal(result, 42); + }); + }); + + describe("#parseTraceparentHeader", () => { + it("returns a new context with spanContext set", () => { + const validTraceparentHeader = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"; + const updatedContext = instrumenter.parseTraceparentHeader(validTraceparentHeader); + assert.exists(updatedContext); + const spanContext = trace.getSpanContext(updatedContext!); + assert.equal(spanContext?.spanId, "00f067aa0ba902b7"); + assert.equal(spanContext?.traceId, "4bf92f3577b34da6a3ce929d0e0e4736"); + }); + }); +}); diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/spanWrapper.spec.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/spanWrapper.spec.ts new file mode 100644 index 000000000000..f7a362433745 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/spanWrapper.spec.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { assert } from "chai"; +import { OpenTelemetrySpanWrapper } from "../../src/spanWrapper"; +import { SpanStatusCode } from "@opentelemetry/api"; +import { TestSpan } from "./util/testSpan"; +import { TestTracer } from "./util/testTracer"; + +describe("OpenTelemetrySpanWrapper", () => { + let otSpan: TestSpan; + let span: OpenTelemetrySpanWrapper; + + beforeEach(() => { + otSpan = new TestTracer().startSpan("test"); + span = new OpenTelemetrySpanWrapper(otSpan); + }); + + describe("#setStatus", () => { + describe("with a successful status", () => { + it("sets the status on the span", () => { + span.setStatus({ status: "success" }); + + assert.deepEqual(otSpan.status, { code: SpanStatusCode.OK }); + }); + }); + + describe("with an error", () => { + it("sets the failed status on the span", () => { + span.setStatus({ status: "error" }); + + assert.deepEqual(otSpan.status, { code: SpanStatusCode.ERROR }); + }); + + it("records the exception if provided", () => { + const error = new Error("test"); + span.setStatus({ status: "error", error }); + + assert.deepEqual(otSpan.exception, error); + }); + }); + }); + + describe("#setAttribute", () => { + it("records the attribute on the span", () => { + span.setAttribute("test", "value"); + span.setAttribute("array", ["value"]); + + assert.deepEqual(otSpan.attributes, { test: "value", array: ["value"] }); + }); + + it("ignores null", () => { + span.setAttribute("test", null); + + assert.isEmpty(otSpan.attributes); + }); + + it("ignores undefined", () => { + span.setAttribute("test", undefined); + + assert.isEmpty(otSpan.attributes); + }); + }); + + describe("#end", () => { + it("ends the wrapped span", () => { + span.end(); + + assert.isTrue(otSpan.endCalled); + }); + }); + + describe("#recordException", () => { + it("sets the error on the wrapped span", () => { + const error = new Error("test"); + span.recordException(error); + + assert.deepEqual(otSpan.exception, error); + }); + it("does not change the status", () => { + const error = "test"; + span.recordException(error); + + assert.deepEqual(otSpan.status, { code: SpanStatusCode.UNSET }); + }); + }); + + describe("#isRecording", () => { + it("returns the value of the wrapped span", () => { + assert.equal(span.isRecording(), otSpan.isRecording()); + }); + }); +}); diff --git a/sdk/core/core-tracing/test/util/testSpan.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testSpan.ts similarity index 84% rename from sdk/core/core-tracing/test/util/testSpan.ts rename to sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testSpan.ts index 8ff22477ceb9..95f131aec018 100644 --- a/sdk/core/core-tracing/test/util/testSpan.ts +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testSpan.ts @@ -2,17 +2,17 @@ // Licensed under the MIT license. import { - Span, - SpanAttributeValue, - SpanAttributes, - SpanContext, + TimeInput, + Tracer, SpanKind, - SpanOptions, SpanStatus, + SpanContext, + SpanAttributes, SpanStatusCode, - TimeInput, - Tracer, -} from "../../src/interfaces"; + SpanAttributeValue, + Span, + Link, +} from "@opentelemetry/api"; /** * A mock span useful for testing. @@ -56,6 +56,16 @@ export class TestSpan implements Span { private _context: SpanContext; private readonly _tracer: Tracer; + /** + * The recorded exception, if any. + */ + exception?: Error; + + /** + * Any links provided when creating this span. + */ + links: Link[]; + /** * Starts a new Span. * @param parentTracer- The tracer that created this Span @@ -69,20 +79,24 @@ export class TestSpan implements Span { parentTracer: Tracer, name: string, context: SpanContext, + kind: SpanKind, parentSpanId?: string, - options?: SpanOptions + startTime: TimeInput = Date.now(), + attributes: SpanAttributes = {}, + links: Link[] = [] ) { this._tracer = parentTracer; this.name = name; - this.kind = options?.kind || SpanKind.INTERNAL; - this.startTime = options?.startTime || Date.now(); + this.kind = kind; + this.startTime = startTime; this.parentSpanId = parentSpanId; - this.attributes = options?.attributes || {}; this.status = { - code: SpanStatusCode.OK, + code: SpanStatusCode.UNSET, }; this.endCalled = false; this._context = context; + this.attributes = attributes; + this.links = links; } /** @@ -148,8 +162,8 @@ export class TestSpan implements Span { addEvent(): this { throw new Error("Method not implemented."); } - recordException(): void { - throw new Error("Method not implemented."); + recordException(exception: Error): void { + this.exception = exception; } updateName(): this { throw new Error("Method not implemented."); diff --git a/sdk/core/core-tracing/test/util/testTracer.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracer.ts similarity index 88% rename from sdk/core/core-tracing/test/util/testTracer.ts rename to sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracer.ts index 1b9d6102aa88..ccdfbca28a00 100644 --- a/sdk/core/core-tracing/test/util/testTracer.ts +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracer.ts @@ -1,16 +1,17 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +import { TestSpan } from "./testSpan"; import { - Context as OTContext, SpanContext, + SpanKind, SpanOptions, TraceFlags, + Context, + context, Tracer, - getSpanContext, - context as otContext, -} from "../../src/interfaces"; -import { TestSpan } from "./testSpan"; + trace, +} from "@opentelemetry/api"; /** * Simple representation of a Span that only has name and child relationships. @@ -124,8 +125,8 @@ export class TestTracer implements Tracer { * @param name - The name of the span. * @param options - The SpanOptions used during Span creation. */ - startSpan(name: string, options?: SpanOptions, context?: OTContext): TestSpan { - const parentContext = getSpanContext(context || otContext.active()); + startSpan(name: string, options?: SpanOptions, currentContext?: Context): TestSpan { + const parentContext = trace.getSpanContext(currentContext || context.active()); let traceId: string; let isRootSpan = false; @@ -142,7 +143,16 @@ export class TestTracer implements Tracer { spanId: this.getNextSpanId(), traceFlags: TraceFlags.NONE, }; - const span = new TestSpan(this, name, spanContext, parentContext?.spanId, options); + const span = new TestSpan( + this, + name, + spanContext, + options?.kind || SpanKind.INTERNAL, + parentContext ? parentContext.spanId : undefined, + options?.startTime, + options?.attributes, + options?.links + ); this.knownSpans.push(span); if (isRootSpan) { this.rootSpans.push(span); diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracerProvider.ts b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracerProvider.ts new file mode 100644 index 000000000000..cca68d106517 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/test/public/util/testTracerProvider.ts @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { TracerProvider, trace } from "@opentelemetry/api"; +import { TestTracer } from "./testTracer"; + +export class TestTracerProvider implements TracerProvider { + private tracer = new TestTracer(); + + getTracer(): TestTracer { + return this.tracer; + } + + register(): boolean { + return trace.setGlobalTracerProvider(this); + } + + disable(): void { + trace.disable(); + } + + setTracer(tracer: TestTracer): void { + this.tracer = tracer; + } +} + +let tracerProvider: TestTracerProvider; + +export function setTracer(tracer?: TestTracer): TestTracer { + resetTracer(); + tracerProvider = new TestTracerProvider(); + tracerProvider.register(); + if (tracer) { + tracerProvider.setTracer(tracer); + } + return tracerProvider.getTracer(); +} + +export function resetTracer(): void { + tracerProvider?.disable(); +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tests.yml b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tests.yml new file mode 100644 index 000000000000..cbf7ddd903d8 --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tests.yml @@ -0,0 +1,11 @@ +trigger: none + +stages: + - template: /eng/pipelines/templates/stages/archetype-sdk-tests.yml + parameters: + PackageName: "@azure/opentelemetry-instrumentation-azure-sdk" + ServiceDirectory: instrumentation + EnvVars: + AZURE_CLIENT_ID: $(aad-azure-sdk-test-client-id) + AZURE_TENANT_ID: $(aad-azure-sdk-test-tenant-id) + AZURE_CLIENT_SECRET: $(aad-azure-sdk-test-client-secret) diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsconfig.json b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsconfig.json new file mode 100644 index 000000000000..072f6025f15d --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../../tsconfig.package", + "compilerOptions": { + "outDir": "./dist-esm", + "declarationDir": "./types", + "paths": { + "@azure/opentelemetry-instrumentation-azure-sdk": ["./src/index"] + } + }, + "include": ["src/**/*.ts", "test/**/*.ts", "samples-dev/**/*.ts"] +} diff --git a/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsdoc.json b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsdoc.json new file mode 100644 index 000000000000..81c5a8a2aa2f --- /dev/null +++ b/sdk/instrumentation/opentelemetry-instrumentation-azure-sdk/tsdoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": ["../../../tsdoc.json"] +} diff --git a/sdk/test-utils/test-utils/package.json b/sdk/test-utils/test-utils/package.json index 9fa9106ad390..98d3ada24c32 100644 --- a/sdk/test-utils/test-utils/package.json +++ b/sdk/test-utils/test-utils/package.json @@ -59,13 +59,13 @@ "mocha": "^7.1.1", "@azure-tools/test-recorder": "^1.0.0", "tslib": "^2.2.0", - "@azure/core-tracing": "1.0.0-preview.13" + "@azure/core-tracing": "1.0.0-preview.14", + "@opentelemetry/api": "^1.0.3" }, "devDependencies": { "@azure/dev-tool": "^1.0.0", "@azure/eslint-plugin-azure-sdk": "^3.0.0", "@microsoft/api-extractor": "^7.18.11", - "@opentelemetry/api": "^1.0.1", "@types/chai": "^4.1.6", "@types/mocha": "^7.0.2", "@types/node": "^12.0.0", diff --git a/sdk/test-utils/test-utils/src/index.ts b/sdk/test-utils/test-utils/src/index.ts index 7398906fbb16..0a4ce96c90d9 100644 --- a/sdk/test-utils/test-utils/src/index.ts +++ b/sdk/test-utils/test-utils/src/index.ts @@ -9,10 +9,14 @@ export { TestFunctionWrapper, } from "./multiVersion"; +export { chaiAzureTrace } from "./tracing/chaiAzureTrace"; export { matrix } from "./matrix"; export { isNode, isNode8 } from "./utils"; export { getYieldedValue } from "./getYieldedValue"; - export { TestSpan } from "./tracing/testSpan"; + +export * from "./tracing/mockInstrumenter"; +export * from "./tracing/mockTracingSpan"; export * from "./tracing/testTracer"; export * from "./tracing/testTracerProvider"; +export * from "./tracing/spanGraphModel"; diff --git a/sdk/test-utils/test-utils/src/tracing/chaiAzureTrace.ts b/sdk/test-utils/test-utils/src/tracing/chaiAzureTrace.ts new file mode 100644 index 000000000000..6cc52dc60f30 --- /dev/null +++ b/sdk/test-utils/test-utils/src/tracing/chaiAzureTrace.ts @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { OperationTracingOptions, useInstrumenter } from "@azure/core-tracing"; +import { assert } from "chai"; +import { MockInstrumenter } from "./mockInstrumenter"; +import { MockTracingSpan } from "./mockTracingSpan"; +import { SpanGraph, SpanGraphNode } from "./spanGraphModel"; + +/** + * Augments Chai with support for Azure Tracing functionality + * + * Sample usage: + * + * ```ts + * import chai from "chai"; + * import { chaiAzureTrace } from "@azure/test-utils"; + * chai.use(chaiAzureTrace); + * + * it("supportsTracing", async () => { + * await assert.supportsTracing((updatedOptions) => myClient.doSomething(updatedOptions), ["myClient.doSomething"]); + * }); + * ``` + * @param chai - The Chai instance + */ +function chaiAzureTrace(chai: Chai.ChaiStatic): void { + // expect(() => {}).to.supportsTracing() syntax + chai.Assertion.addMethod("supportTracing", function < + T + >(this: Chai.AssertionStatic, expectedSpanNames: string[], options?: T) { + return assert.supportsTracing(this._obj, expectedSpanNames, options, this._obj); + }); + + // assert.supportsTracing(() => {}) syntax + chai.assert.supportsTracing = supportsTracing; +} + +const instrumenter = new MockInstrumenter(); +/** + * The supports Tracing function does the verification of whether the core-tracing is supported correctly with the client method + * This function verifies the root span, if all the correct spans are called as expected and if they are closed. + * @param callback - Callback function of the client that should be invoked + * @param expectedSpanNames - List of span names that are expected to be generated + * @param options - Options for either Core HTTP operations or custom options for the callback + * @param thisArg - optional this parameter for the callback + */ +async function supportsTracing< + Options extends { tracingOptions?: OperationTracingOptions }, + Callback extends (options: Options) => Promise +>( + callback: Callback, + expectedSpanNames: string[], + options?: Options, + thisArg?: ThisParameterType +) { + useInstrumenter(instrumenter); + instrumenter.reset(); + const startSpanOptions = { + packageName: "test", + ...options, + }; + const { span: rootSpan, tracingContext } = instrumenter.startSpan("root", startSpanOptions); + + const newOptions = { + ...options, + tracingOptions: { + tracingContext: tracingContext, + }, + } as Options; + await callback.call(thisArg, newOptions); + rootSpan.end(); + const spanGraph = getSpanGraph((rootSpan as MockTracingSpan).traceId, instrumenter); + assert.equal(spanGraph.roots.length, 1, "There should be just one root span"); + assert.equal(spanGraph.roots[0].name, "root"); + assert.strictEqual( + rootSpan, + instrumenter.startedSpans[0], + "The root span should match what was passed in." + ); + + const directChildren = spanGraph.roots[0].children.map((child) => child.name); + assert.sameMembers(Array.from(new Set(directChildren)), expectedSpanNames); + rootSpan.end(); + const openSpans = instrumenter.startedSpans.filter((s) => !s.endCalled); + assert.equal( + openSpans.length, + 0, + `All spans should have been closed, but found ${openSpans.map((s) => s.name)} open spans.` + ); +} + +/** + * Return all Spans for a particular trace, grouped by their + * parent Span in a tree-like structure + * @param traceId - The traceId to return the graph for + */ +function getSpanGraph(traceId: string, instrumenter: MockInstrumenter): SpanGraph { + const traceSpans = instrumenter.startedSpans.filter((span) => { + return span.traceId === traceId; + }); + + const roots: SpanGraphNode[] = []; + const nodeMap: Map = new Map(); + + for (const span of traceSpans) { + const spanId = span.spanId; + const node: SpanGraphNode = { + name: span.name, + children: [], + }; + nodeMap.set(spanId, node); + + if (span.parentSpan()?.spanId) { + const parentSpan = span.parentSpan()?.spanId; + const parent = nodeMap.get(parentSpan!); + if (!parent) { + throw new Error( + `Span with name ${node.name} has an unknown parentSpan with id ${parentSpan}` + ); + } + parent.children.push(node); + } else { + roots.push(node); + } + } + + return { + roots, + }; +} + +/* eslint-disable @typescript-eslint/no-namespace */ +declare global { + export namespace Chai { + interface Assertion { + supportTracing(expectedSpanNames: string[], options?: T): Promise; + } + interface Assert { + supportsTracing< + Options extends { tracingOptions?: OperationTracingOptions }, + Callback extends (options: Options) => Promise + >( + callback: Callback, + expectedSpanNames: string[], + options?: Options, + thisArg?: ThisParameterType + ): Promise; + } + } +} + +export { chaiAzureTrace }; diff --git a/sdk/test-utils/test-utils/src/tracing/mockContext.ts b/sdk/test-utils/test-utils/src/tracing/mockContext.ts new file mode 100644 index 000000000000..972666a643f5 --- /dev/null +++ b/sdk/test-utils/test-utils/src/tracing/mockContext.ts @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { TracingContext } from "@azure/core-tracing"; + +/** + * This is the implementation of the {@link TracingContext} interface + * Represents a tracing context + */ +export class MockContext implements TracingContext { + /** + * Represents a context map for the symbols to record + */ + private contextMap: Map; + + /** + * Initializes the context map + * @param parentContext - If present the context map is initialized to the contextMap of the parentContext + */ + constructor(parentContext?: TracingContext) { + if (parentContext && !(parentContext instanceof MockContext)) { + throw new Error("received parent context, but it is not mock context..."); + } + this.contextMap = new Map(parentContext?.contextMap || new Map()); + } + + setValue(key: symbol, value: unknown): TracingContext { + const newContext = new MockContext(this); + newContext.contextMap.set(key, value); + return newContext; + } + + getValue(key: symbol): unknown { + return this.contextMap.get(key); + } + + deleteValue(key: symbol): TracingContext { + const newContext = new MockContext(this); + newContext.contextMap.delete(key); + return newContext; + } +} + +export const spanKey = Symbol.for("span"); diff --git a/sdk/test-utils/test-utils/src/tracing/mockInstrumenter.ts b/sdk/test-utils/test-utils/src/tracing/mockInstrumenter.ts new file mode 100644 index 000000000000..e7d55690ec0d --- /dev/null +++ b/sdk/test-utils/test-utils/src/tracing/mockInstrumenter.ts @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + Instrumenter, + InstrumenterSpanOptions, + TracingContext, + TracingSpan, +} from "@azure/core-tracing"; +import { MockContext, spanKey } from "./mockContext"; +import { MockTracingSpan } from "./mockTracingSpan"; + +/** + * Represents an implementation of {@link Instrumenter} interface that keeps track of the tracing contexts and spans + */ +export class MockInstrumenter implements Instrumenter { + /** + * Stack of immutable contexts, each of which is a bag of tracing values for the current operation + */ + public contextStack: TracingContext[] = [new MockContext()]; + /** + * List of started spans + */ + public startedSpans: MockTracingSpan[] = []; + + private traceIdCounter = 0; + private getNextTraceId(): string { + this.traceIdCounter++; + return this.traceIdCounter.toString().padStart(32, "0"); + } + + private spanIdCounter = 0; + private getNextSpanId(): string { + this.spanIdCounter++; + return this.spanIdCounter.toString().padStart(16, "0"); + } + + startSpan( + name: string, + spanOptions?: InstrumenterSpanOptions + ): { span: TracingSpan; tracingContext: TracingContext } { + const tracingContext = spanOptions?.tracingContext || this.currentContext(); + const parentSpan = tracingContext.getValue(spanKey) as MockTracingSpan | undefined; + let traceId; + if (parentSpan) { + traceId = parentSpan.traceId; + } else { + traceId = this.getNextTraceId(); + } + + const spanContext = { + spanId: this.getNextSpanId(), + traceId: traceId, + traceFlags: 0, + }; + const span = new MockTracingSpan( + name, + spanContext.traceId, + spanContext.spanId, + tracingContext, + spanOptions + ); + let context: TracingContext = new MockContext(tracingContext); + context = context.setValue(spanKey, span); + + this.startedSpans.push(span); + return { span, tracingContext: context }; + } + + withContext< + CallbackArgs extends unknown[], + Callback extends (...args: CallbackArgs) => ReturnType + >( + context: TracingContext, + callback: Callback, + ...callbackArgs: CallbackArgs + ): ReturnType { + this.contextStack.push(context); + return Promise.resolve(callback(...callbackArgs)).finally(() => { + this.contextStack.pop(); + }) as ReturnType; + } + + parseTraceparentHeader(_traceparentHeader: string): TracingContext | undefined { + return; + } + + createRequestHeaders(_tracingContext: TracingContext): Record { + return {}; + } + + /** + * Gets the currently active context. + * + * @returns The current context. + */ + currentContext() { + return this.contextStack[this.contextStack.length - 1]; + } + + /** + * Resets the state of the instrumenter to a clean slate. + */ + reset() { + this.contextStack = [new MockContext()]; + this.startedSpans = []; + this.traceIdCounter = 0; + this.spanIdCounter = 0; + } +} diff --git a/sdk/test-utils/test-utils/src/tracing/mockTracingSpan.ts b/sdk/test-utils/test-utils/src/tracing/mockTracingSpan.ts new file mode 100644 index 000000000000..4ebcfd905f0b --- /dev/null +++ b/sdk/test-utils/test-utils/src/tracing/mockTracingSpan.ts @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { + TracingSpan, + SpanStatus, + TracingSpanOptions, + TracingSpanKind, + TracingContext, +} from "@azure/core-tracing"; +import { spanKey } from "./mockContext"; + +/** + * Represents an implementation of a mock tracing span {@link TracingSpan} used for tests + */ +export class MockTracingSpan implements TracingSpan { + /** + * Name of the current span + */ + name: string; + /** + * Kind of the current span {@link TracingSpanKind} + */ + spanKind?: TracingSpanKind; + /** + * Existing or parent tracing context + */ + tracingContext?: TracingContext; + + /** + * The generated ID of the span within a given trace + */ + spanId: string; + + /** + * The ID of the trace this span belongs to + */ + traceId: string; + + /** + * + * @param name - Name of the current span + * @param spanContext - A unique, serializable identifier for a span + * @param tracingContext - Existing or parent tracing context + * @param spanOptions - Options to configure the newly created span {@link TracingSpanOptions} + */ + constructor( + name: string, + traceId: string, + spanId: string, + tracingContext?: TracingContext, + spanOptions?: TracingSpanOptions + ) { + this.name = name; + this.spanKind = spanOptions?.spanKind; + this.tracingContext = tracingContext; + this.traceId = traceId; + this.spanId = spanId; + } + + spanStatus?: SpanStatus; + attributes: Record = {}; + endCalled = false; + exception?: string | Error; + setStatus(status: SpanStatus): void { + this.spanStatus = status; + } + setAttribute(name: string, value: unknown): void { + this.attributes[name] = value; + } + end(): void { + this.endCalled = true; + } + recordException(exception: string | Error): void { + this.exception = exception; + } + + isRecording(): boolean { + return true; + } + + parentSpan(): MockTracingSpan | undefined { + return this.tracingContext?.getValue(spanKey) as MockTracingSpan; + } +} diff --git a/sdk/test-utils/test-utils/src/tracing/spanGraphModel.ts b/sdk/test-utils/test-utils/src/tracing/spanGraphModel.ts new file mode 100644 index 000000000000..a97c2b019298 --- /dev/null +++ b/sdk/test-utils/test-utils/src/tracing/spanGraphModel.ts @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Simple representation of a Span that only has name and child relationships. + * Children should be arranged in the order they were created. + */ +export interface SpanGraphNode { + /** + * The Span name + */ + name: string; + /** + * All child Spans of this Span + */ + children: SpanGraphNode[]; +} + +/** + * Contains all the spans for a particular TraceID + * starting at unparented roots + */ +export interface SpanGraph { + /** + * All Spans without a parentSpanId + */ + roots: SpanGraphNode[]; +} diff --git a/sdk/test-utils/test-utils/src/tracing/testSpan.ts b/sdk/test-utils/test-utils/src/tracing/testSpan.ts index 30b1230f00e0..363c39b9edef 100644 --- a/sdk/test-utils/test-utils/src/tracing/testSpan.ts +++ b/sdk/test-utils/test-utils/src/tracing/testSpan.ts @@ -2,17 +2,16 @@ // Licensed under the MIT license. import { - TimeInput, - Tracer, + Span, + SpanAttributeValue, + SpanAttributes, + SpanContext, SpanKind, + Tracer, + TimeInput, SpanStatus, - SpanContext, - SpanAttributes, SpanStatusCode, - SpanAttributeValue, - Span, -} from "@azure/core-tracing"; - +} from "@opentelemetry/api"; /** * A mock span useful for testing. */ @@ -78,9 +77,7 @@ export class TestSpan implements Span { this.kind = kind; this.startTime = startTime; this.parentSpanId = parentSpanId; - this.status = { - code: SpanStatusCode.OK, - }; + this.status = { code: SpanStatusCode.OK }; this.endCalled = false; this._context = context; this.attributes = attributes; diff --git a/sdk/test-utils/test-utils/src/tracing/testTracer.ts b/sdk/test-utils/test-utils/src/tracing/testTracer.ts index 8027e122428e..7cc2eaadaf94 100644 --- a/sdk/test-utils/test-utils/src/tracing/testTracer.ts +++ b/sdk/test-utils/test-utils/src/tracing/testTracer.ts @@ -9,35 +9,10 @@ import { TraceFlags, Context as OTContext, context as otContext, - getSpanContext, Tracer, -} from "@azure/core-tracing"; - -/** - * Simple representation of a Span that only has name and child relationships. - * Children should be arranged in the order they were created. - */ -export interface SpanGraphNode { - /** - * The Span name - */ - name: string; - /** - * All child Spans of this Span - */ - children: SpanGraphNode[]; -} - -/** - * Contains all the spans for a particular TraceID - * starting at unparented roots - */ -export interface SpanGraph { - /** - * All Spans without a parentSpanId - */ - roots: SpanGraphNode[]; -} + trace as otTrace, +} from "@opentelemetry/api"; +import { SpanGraph, SpanGraphNode } from "./spanGraphModel"; /** * A mock tracer useful for testing @@ -167,3 +142,39 @@ export class TestTracer implements Tracer { throw new Error("Method not implemented."); } } + +/** + * Get the span context of the span if it exists. + * + * @param context - context to get values from + */ +export function getSpanContext(context: Context): SpanContext | undefined { + return otTrace.getSpanContext(context); +} + +/** + * OpenTelemetry compatible interface for Context + */ +export interface Context { + /** + * Get a value from the context. + * + * @param key - key which identifies a context value + */ + getValue(key: symbol): unknown; + /** + * Create a new context which inherits from this context and has + * the given key set to the given value. + * + * @param key - context key for which to set the value + * @param value - value to set for the given key + */ + setValue(key: symbol, value: unknown): Context; + /** + * Return a new context which inherits from this context but does + * not contain a value for the given key. + * + * @param key - context key for which to clear a value + */ + deleteValue(key: symbol): Context; +} diff --git a/sdk/test-utils/test-utils/src/tracing/testTracerProvider.ts b/sdk/test-utils/test-utils/src/tracing/testTracerProvider.ts index 4874d8c7aff9..92c6f2d58bf2 100644 --- a/sdk/test-utils/test-utils/src/tracing/testTracerProvider.ts +++ b/sdk/test-utils/test-utils/src/tracing/testTracerProvider.ts @@ -4,9 +4,25 @@ import { TestTracer } from "./testTracer"; // This must be the same as the default tracer name supplied from @azure/core-tracing. const TRACER_NAME = "azure/core-tracing"; +/** + * Implementation for TracerProvider from opentelemetry/api package. + * It is a registry for creating named tracers. + * This is exported only so that we can support packages using @azure/core-tracing <= 1.0.0-preview.13 + * while transitioning to @azure/core-tracing >= 1.0.0-preview.14 + */ export class TestTracerProvider implements TracerProvider { private tracerCache: Map = new Map(); - + /** + * Returns a Tracer, creating one if one with the given name and version is + * not already created. + * + * This function may return different Tracer types (e.g. + * NoopTracerProvider vs. a functional tracer). + * + * @param name The name of the tracer or instrumentation library. + * @param version The version of the tracer or instrumentation library. + * @returns Tracer A Tracer with the given name and version + */ getTracer(name: string, _version?: string): TestTracer { if (!this.tracerCache.has(name)) { this.tracerCache.set(name, new TestTracer(name, name)); @@ -14,15 +30,21 @@ export class TestTracerProvider implements TracerProvider { return this.tracerCache.get(name)!; } - register() { + /** + * Registers the current tracer provider + */ + register(): void { trace.setGlobalTracerProvider(this); } - disable() { + /** + * Removes global trace provider + */ + disable(): void { trace.disable(); } - setTracer(tracer: TestTracer) { + setTracer(tracer: TestTracer): void { this.tracerCache.set(TRACER_NAME, tracer); } } diff --git a/sdk/test-utils/test-utils/test/tracing/mockInstrumenter.spec.ts b/sdk/test-utils/test-utils/test/tracing/mockInstrumenter.spec.ts new file mode 100644 index 000000000000..580bb5f9d770 --- /dev/null +++ b/sdk/test-utils/test-utils/test/tracing/mockInstrumenter.spec.ts @@ -0,0 +1,130 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { createTracingClient, TracingClient, useInstrumenter } from "@azure/core-tracing"; +import { MockTracingSpan, MockInstrumenter } from "../../src"; +import chai, { assert, expect } from "chai"; +import { chaiAzureTrace } from "../../src/tracing/chaiAzureTrace"; +import { MockContext } from "../../src/tracing/mockContext"; +import { OperationTracingOptions } from "@azure/core-tracing"; +chai.use(chaiAzureTrace); + +describe("TestInstrumenter", function () { + let instrumenter: MockInstrumenter; + + beforeEach(function () { + instrumenter = new MockInstrumenter(); + }); + + describe("#startSpan", function () { + it("starts a span and adds to startedSpans array", function () { + const { span } = instrumenter.startSpan("testSpan"); + assert.equal(instrumenter.startedSpans.length, 1); + assert.equal(instrumenter.startedSpans[0], span as MockTracingSpan); + assert.equal(instrumenter.startedSpans[0].name, "testSpan"); + }); + + it("returns a new context with existing attributes", function () { + const existingContext = new MockContext().setValue(Symbol.for("foo"), "bar"); + + const { tracingContext: newContext } = instrumenter.startSpan("testSpan", { + packageName: "test", + tracingContext: existingContext, + }); + + assert.equal(newContext.getValue(Symbol.for("foo")), "bar"); + }); + }); + + describe("#withContext", function () { + it("sets the active context in synchronous functions", async function () { + const { tracingContext } = instrumenter.startSpan("contextTest"); + // TODO: figure out how to be smarter about not wrapping sync functions in promise... + const result = await instrumenter.withContext(tracingContext, function () { + assert.equal(instrumenter.currentContext(), tracingContext); + return 42; + }); + + assert.equal(result, 42); + assert.notEqual(instrumenter.currentContext(), tracingContext); + }); + + it("sets the active context during async functions", async function () { + const { tracingContext } = instrumenter.startSpan("contextTest"); + const result = await instrumenter.withContext(tracingContext, async function () { + await new Promise((resolve) => setTimeout(resolve, 1000)); + assert.equal(instrumenter.currentContext(), tracingContext); + return 42; + }); + assert.equal(result, 42); + assert.notEqual(instrumenter.currentContext(), tracingContext); + }); + + it("resets the previous context after the function returns", async function () { + const existingContext = instrumenter.currentContext(); + const { tracingContext } = instrumenter.startSpan("test"); + await instrumenter.withContext(tracingContext, async function () { + // no-op + }); + assert.equal(instrumenter.currentContext(), existingContext); + }); + }); +}); + +describe("TestInstrumenter with MockClient", function () { + let instrumenter: MockInstrumenter; + let client: MockClientToTest; + + beforeEach(function () { + instrumenter = new MockInstrumenter(); + useInstrumenter(instrumenter); + client = new MockClientToTest(); + }); + + it("starts a span and adds to startedSpans array", async function () { + await client.method(); + assert.equal(instrumenter.startedSpans.length, 1); + assert.equal(instrumenter.startedSpans[0].name, "MockClientToTest.method"); + }); +}); + +describe("Test supportsTracing plugin functionality", function () { + let client: MockClientToTest; + beforeEach(function () { + client = new MockClientToTest(); + }); + + it("supportsTracing with assert", async function () { + await assert.supportsTracing((options) => client.method(options), ["MockClientToTest.method"]); + }); + + it("supportsTracing with expect", async function () { + await expect((options: any) => client.method(options)).to.supportTracing([ + "MockClientToTest.method", + ]); + }); +}); + +/** + * Represent a convenience client that has enabled tracing on a single method. + * Used for testing assertions. + */ +export class MockClientToTest { + public record: Record; + tracingClient: TracingClient; + + constructor() { + this.record = {}; + this.tracingClient = createTracingClient({ + namespace: "Microsoft.Test", + packageName: "@azure/test", + packageVersion: "foobar", + }); + } + + async method(options?: Options) { + return this.tracingClient.withSpan("MockClientToTest.method", options || {}, () => 42, { + spanKind: "consumer", + }); + } +} diff --git a/sdk/test-utils/test-utils/test/tracing/mockTracingSpan.spec.ts b/sdk/test-utils/test-utils/test/tracing/mockTracingSpan.spec.ts new file mode 100644 index 000000000000..80c812cd84e8 --- /dev/null +++ b/sdk/test-utils/test-utils/test/tracing/mockTracingSpan.spec.ts @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { MockTracingSpan } from "../../src"; +import { assert } from "chai"; + +describe("TestTracingSpan", function () { + let subject: MockTracingSpan; + + beforeEach(() => { + subject = new MockTracingSpan("test", "traceId", "spanId"); + }); + + it("records status correctly", function () { + subject.setStatus({ status: "success" }); + assert.deepEqual(subject.spanStatus, { status: "success" }); + }); + + it("records attributes correctly", async function () { + subject.setAttribute("attribute1", "value1"); + subject.setAttribute("attribute2", "value2"); + assert.equal(subject.attributes["attribute1"], "value1"); + assert.equal(subject.attributes["attribute2"], "value2"); + }); + + it("records calls to `end` correctly", function () { + assert.equal(subject.endCalled, false); + subject.end(); + assert.equal(subject.endCalled, true); + }); + + it("records exceptions", function () { + const expectedException = new Error("foo"); + subject.recordException(expectedException); + assert.strictEqual(subject.exception, expectedException); + }); +});