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); + }); +});