├── .changeset ├── README.md ├── config.json ├── nine-gorillas-happen.md └── tricky-kids-look.md ├── .editorconfig ├── .eslintrc.cjs ├── .github └── workflows │ ├── pr.yml │ ├── push.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.js ├── examples ├── basic │ ├── basic.js │ ├── basic.ts │ ├── package-lock.json │ ├── package.json │ └── wrangler.toml ├── service-binding │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── wrangler.toml ├── span-builder │ ├── package-lock.json │ ├── package.json │ ├── src │ │ └── index.ts │ └── wrangler.toml └── zipkin-basic │ ├── index.ts │ ├── package-lock.json │ ├── package.json │ └── wrangler.toml ├── package-lock.json ├── package.json ├── src ├── builder.ts ├── index.ts ├── trace.ts ├── tracing.ts ├── transformers │ ├── otlp.ts │ ├── transformer.ts │ └── zipkin.ts ├── types.ts └── utils │ ├── constants.ts │ └── rand.ts ├── test ├── api.test.ts ├── otlp-exporter.test.ts ├── scripts │ ├── api │ │ ├── root-span-attributes.ts │ │ ├── root-span-events.ts │ │ ├── root-span-links.ts │ │ ├── root-span-resource-attributes.ts │ │ ├── root-span-status.ts │ │ ├── root-span.ts │ │ └── span-builder │ │ │ ├── add-remove-attributes.ts │ │ │ ├── attributes.ts │ │ │ ├── basic.ts │ │ │ ├── event.ts │ │ │ ├── links.ts │ │ │ └── status.ts │ ├── collector.ts │ ├── otlp │ │ ├── basic.ts │ │ ├── multiple-spans-attributes-and-events.ts │ │ ├── multiple-spans-attributes.ts │ │ ├── multiple-spans-events.ts │ │ ├── multiple-spans.ts │ │ ├── resource-attributes.ts │ │ ├── single-span-attributes-and-events.ts │ │ ├── single-span-attributes.ts │ │ ├── single-span-events.ts │ │ ├── single-span.ts │ │ ├── span-span-attributes-and-events.ts │ │ ├── span-span-attributes.ts │ │ ├── span-span-events.ts │ │ └── span-span.ts │ └── zipkin │ │ ├── basic.ts │ │ ├── multiple-spans-attributes-and-events.ts │ │ ├── multiple-spans-attributes.ts │ │ ├── multiple-spans-events.ts │ │ ├── multiple-spans.ts │ │ ├── resource-attributes.ts │ │ ├── single-span-attributes-and-events.ts │ │ ├── single-span-attributes.ts │ │ ├── single-span-events.ts │ │ ├── single-span.ts │ │ ├── span-span-attributes-and-events.ts │ │ ├── span-span-attributes.ts │ │ ├── span-span-events.ts │ │ └── span-span.ts ├── utils │ ├── trace.ts │ └── worker.ts └── zipkin-exporter.test.ts ├── tsconfig.emit.json ├── tsconfig.json └── vitest.config.ts /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@2.3.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "BlobDevelopment/workers-tracing" } 6 | ], 7 | "commit": false, 8 | "fixed": [], 9 | "linked": [], 10 | "access": "public", 11 | "baseBranch": "main", 12 | "updateInternalDependencies": "patch", 13 | "ignore": [] 14 | } 15 | -------------------------------------------------------------------------------- /.changeset/nine-gorillas-happen.md: -------------------------------------------------------------------------------- 1 | --- 2 | "workers-tracing": patch 3 | --- 4 | 5 | Added a span builder, this will allow for a more friendly experience. 6 | 7 | Example usage: 8 | ``` 9 | const span = trace.buildSpan(SPAN_NAME.KV_GET) 10 | .addAttribute('Index', i) 11 | .addAttribute(ATTRIBUTE_NAME.KV_KEY, `id:${i}`) 12 | .addLink(forLoopSpan); 13 | 14 | span.end(); 15 | ``` 16 | -------------------------------------------------------------------------------- /.changeset/tricky-kids-look.md: -------------------------------------------------------------------------------- 1 | --- 2 | "workers-tracing": patch 3 | --- 4 | 5 | Fixed #9 - if `navigator` is not defined (old compat date) it will now default to "Unknown" for the runtime name. 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 2 6 | tab_width = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | tab_width = 2 14 | end_of_line = lf 15 | insert_final_newline = true 16 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | ], 11 | overrides: [], 12 | parser: '@typescript-eslint/parser', 13 | parserOptions: { 14 | ecmaVersion: 'latest', 15 | sourceType: 'module', 16 | }, 17 | plugins: ['@typescript-eslint', 'import'], 18 | rules: { 19 | indent: ['error', 'tab'], 20 | 'linebreak-style': ['error', 'unix'], 21 | quotes: ['error', 'single'], 22 | semi: ['error', 'always'], 23 | 'comma-dangle': [ 24 | 'error', 25 | { 26 | arrays: 'always-multiline', 27 | objects: 'always-multiline', 28 | imports: 'always-multiline', 29 | exports: 'always-multiline', 30 | functions: 'always-multiline', 31 | }, 32 | ], 33 | 'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1, maxBOF: 0 } ], 34 | 'eol-last': ['error', 'always'], 35 | 'max-len': ['error', { code: 120, tabWidth: 2 }], 36 | '@typescript-eslint/no-empty-interface': ['off'], 37 | 'import/order': [ 38 | 'error', 39 | { 40 | 'groups': [ 41 | 'builtin', 42 | 'external', 43 | 'internal', 44 | 'sibling', 45 | 'parent', 46 | 'index', 47 | 'object', 48 | 'type', 49 | ], 50 | }, 51 | ], 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | 3 | jobs: 4 | test: 5 | name: Checks 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout repository 9 | uses: actions/checkout@v3 10 | 11 | - name: Setup node 12 | uses: actions/setup-node@v3 13 | with: 14 | node-version: 18 15 | cache: 'npm' 16 | 17 | - name: Install deps 18 | run: npm ci 19 | 20 | - name: Lint 21 | run: npm run lint 22 | 23 | - name: Run tests 24 | run: npm run test:ci 25 | 26 | - name: Run build 27 | run: npm run build 28 | -------------------------------------------------------------------------------- /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | 3 | jobs: 4 | test: 5 | name: Tests 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Checkout repository 9 | uses: actions/checkout@v3 10 | 11 | - name: Setup node 12 | uses: actions/setup-node@v3 13 | with: 14 | node-version: 18 15 | cache: 'npm' 16 | 17 | - name: Install deps 18 | run: npm ci 19 | 20 | - name: Lint 21 | run: npm run lint 22 | 23 | - name: Run tests 24 | run: npm run test:ci 25 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | name: Release 11 | runs-on: ubuntu-latest 12 | if: ${{ github.repository_owner == 'BlobDevelopment' }} 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup node 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 18 21 | cache: 'npm' 22 | 23 | - name: Install deps 24 | run: npm ci 25 | 26 | - name: Check size 27 | run: npm run record-sizes 28 | 29 | - name: Create Release PR or publish 30 | uses: changesets/action@v1 31 | with: 32 | publish: npm run publish 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | /testing/ 3 | /tmp/ 4 | /.mf/ 5 | /dist/ 6 | 7 | workers-tracing.todo 8 | **/.DS_Store 9 | kill-stuck-ports.sh 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # workers-tracing 2 | 3 | ## 0.1.3 4 | 5 | ### Patch Changes 6 | 7 | - [#6](https://github.com/BlobDevelopment/workers-tracing/pull/6) [`544a4c0`](https://github.com/BlobDevelopment/workers-tracing/commit/544a4c0668484c7e230bbfe040a3fa1716d27539) Thanks [@WalshyDev](https://github.com/WalshyDev)! - Add PR checks; lint, test and build 8 | 9 | - [#8](https://github.com/BlobDevelopment/workers-tracing/pull/8) [`dee468f`](https://github.com/BlobDevelopment/workers-tracing/commit/dee468f12ff4fe5395d7bcbbc76e44a358d566c1) Thanks [@WalshyDev](https://github.com/WalshyDev)! - Fixed Content-Type not being set 10 | 11 | ## 0.1.2 12 | 13 | ### Patch Changes 14 | 15 | - [`4699a88`](https://github.com/BlobDevelopment/workers-tracing/commit/4699a882fa8595d6535abc48e5b932b2b75d3ad2) Thanks [@WalshyDev](https://github.com/WalshyDev)! - Updated Zipkin example to use the package 16 | 17 | ## 0.1.0 18 | 19 | ### Minor Changes 20 | 21 | - [`02db552`](https://github.com/BlobDevelopment/workers-tracing/commit/02db55226eaec370a4549ded4e32ca9219174b06) Thanks [@WalshyDev](https://github.com/WalshyDev)! - Beta ready! 22 | 23 | ## 0.0.3 24 | 25 | ### Patch Changes 26 | 27 | - [`2a29509`](https://github.com/BlobDevelopment/workers-tracing/commit/2a29509aa0dac8855a45826645e775d329160fc2) Thanks [@WalshyDev](https://github.com/WalshyDev)! - Fix headers being attached to the collector fetch in the case of a non-header object. 28 | 29 | - [`1ea04f8`](https://github.com/BlobDevelopment/workers-tracing/commit/1ea04f828ec04d6d089c9c3a2e6deb3e75f4aeb4) Thanks [@WalshyDev](https://github.com/WalshyDev)! - Add support for doubleValue to OTLP 30 | 31 | ## 0.0.2 32 | 33 | ### Patch Changes 34 | 35 | - [`fab590e`](https://github.com/BlobDevelopment/workers-tracing/commit/fab590e6c2b5f841053df774d01567362a150a48) Thanks [@WalshyDev](https://github.com/WalshyDev)! - Fixed type of `transformer` to be TraceTransformer rather than unknown. Also moved it into `collector` rather than sitting under `TracerOptions` 36 | 37 | - [`ca94f20`](https://github.com/BlobDevelopment/workers-tracing/commit/ca94f208b257c30474006131bfe3be8e8b860839) Thanks [@WalshyDev](https://github.com/WalshyDev)! - Maybe fix changeset publishing to npm for each push 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 BlobDevelopment 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Workers Tracing 2 | 3 | Workers tracing is a small (~2.6 KB compressed), zero-dependency library for having distributed tracing within [Cloudflare Workers](https://workers.cloudflare.com/). 4 | 5 | There are currently 2 different formats supported: 6 | - [OpenTelemetry](https://opentelemetry.io/) is a standard tracing/metrics/logs format. It has wide support in many different services such as [Jaeger](https://www.jaegertracing.io/). 7 | - [Zipkin](https://zipkin.io/) is another widely adopted format which is focused on tracing. 8 | 9 | > **Warning** 10 | > This library is in beta, consider any minor version change a possibly breaking change. I will try to keep compatibiltiy for at least 1 version but cannot guarantee it. 11 | > Please provide feedback in [Issues](https://github.com/BlobDevelopment/workers-tracing/issues) 12 | 13 | > **Note** 14 | > This is an opinionated library, it does not use the standard patterns and base libraries. 15 | > This was done very intentionally, we believe this libary is much cleaner (and just lighter) than the standard libraries. 16 | 17 | ## Install 18 | 19 | Installing this package is easy, you simply need to install the npm package like so: 20 | ``` 21 | npm install --save workers-tracing 22 | ``` 23 | 24 | ## Usage 25 | 26 | ### JavaScript 27 | 28 | ```js 29 | import { createTrace, SPAN_NAME, ATTRIBUTE_NAME } from 'workers-tracing'; 30 | 31 | export default { 32 | async fetch(req, env, ctx) { 33 | const trace = createTrace(req, env, ctx, { 34 | serviceName: 'basic-worker-tracing', 35 | collector: { 36 | url: 'http://localhost:4318/v1/traces', 37 | }, 38 | }); 39 | 40 | return this.handleRequest(req, env, trace); 41 | }, 42 | 43 | async handleRequest(req, env, trace) { 44 | const { pathname } = new URL(req.url); 45 | const span = trace.startSpan('handleRequest', { attributes: { path: pathname } }); 46 | 47 | await env.KV.put('abc', 'def'); 48 | 49 | // .trace will return the value from the passed function 50 | // In this case, it'll return the KV value 51 | const val = await trace.trace(SPAN_NAME.KV_GET, 52 | () => env.KV.get('abc'), 53 | // There are a bunch of built in attribute/span names which you can use 54 | // This will allow you to ensure consistency in naming throughout your Workers 55 | { attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc '} }, 56 | ); 57 | span.addEvent({ name: 'KV lookup', timestamp: Date.now(), attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc' } }); 58 | 59 | span.end(); 60 | await trace.send(); 61 | return new Response(val); 62 | }, 63 | }; 64 | ``` 65 | (see more in the [examples folder](https://github.com/BlobDevelopment/workers-tracing/tree/main/examples)) 66 | 67 | ### TypeScript 68 | 69 | ```ts 70 | import { createTrace, Trace, SPAN_NAME, ATTRIBUTE_NAME } from 'workers-tracing'; 71 | 72 | interface Env { 73 | KV: KVNamespace; 74 | } 75 | 76 | export default { 77 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 78 | const trace = createTrace(req, env, ctx, { 79 | serviceName: 'basic-worker-tracing', 80 | collector: { 81 | url: 'http://localhost:4318/v1/traces', 82 | }, 83 | }); 84 | 85 | return this.handleRequest(req, env, trace); 86 | }, 87 | 88 | async handleRequest(req: Request, env: Env, trace: Trace) { 89 | const { pathname } = new URL(req.url); 90 | const span = trace.startSpan('handleRequest', { attributes: { path: pathname } }); 91 | 92 | await env.KV.put('abc', 'def'); 93 | 94 | // .trace will return the value from the passed function 95 | // In this case, it'll return the KV value 96 | const val = await trace.trace(SPAN_NAME.KV_GET, 97 | () => env.KV.get('abc'), 98 | { attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc '} }, 99 | ); 100 | span.addEvent({ name: 'KV lookup', timestamp: Date.now(), attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc' } }); 101 | 102 | span.end(); 103 | await trace.send(); 104 | return new Response(val); 105 | }, 106 | }; 107 | ``` 108 | (see more in the [examples folder](https://github.com/BlobDevelopment/workers-tracing/tree/main/examples)) 109 | 110 | ### Jaeger 111 | 112 | To send traces to the Jaeger [OpenTelemetry compatible collector](https://www.jaegertracing.io/docs/1.40/deployment/#collector) you will need to make sure Jaeger is configured to accept [OpenTelemetry](https://opentelemetry.io/) or [Zipkin](https://zipkin.io/). 113 | 114 | For [OpenTelemetry](https://opentelemetry.io/) you will need to enable the compatibility support with `COLLECTOR_OTLP_ENABLED=true` (and make sure port `4318` is mapped). 115 | 116 | For [Zipkin](https://zipkin.io/) you will need to enable the JSON compatible layer by setting `COLLECTOR_ZIPKIN_HOST_PORT=:9411` (or a different port - make sure to map this). 117 | 118 | Here is an example command to run the `all-in-one` Docker image with the [OpenTelemetry](https://opentelemetry.io/) and [Zipkin](https://zipkin.io/) compatible collector enabled: 119 | ```sh 120 | $ docker run -d --name jaeger \ 121 | -e COLLECTOR_ZIPKIN_HOST_PORT=:9411 \ 122 | -e COLLECTOR_OTLP_ENABLED=true \ 123 | -p 6831:6831/udp \ 124 | -p 6832:6832/udp \ 125 | -p 5778:5778 \ 126 | -p 16686:16686 \ 127 | -p 4317:4317 \ 128 | -p 4318:4318 \ 129 | -p 14250:14250 \ 130 | -p 14268:14268 \ 131 | -p 14269:14269 \ 132 | -p 9411:9411 \ 133 | jaegertracing/all-in-one:1.40 134 | ``` 135 | 136 | (or through their binary: https://www.jaegertracing.io/download/ - `COLLECTOR_ZIPKIN_HOST_PORT=:9411 COLLECTOR_OTLP_ENABLED=true ./jaeger-all-in-one`) 137 | 138 | Once that is up, just set your collector URL in your Worker. 139 | Here's an example of sending to the OTLP compatible endpoint: 140 | ```js 141 | const trace = createTrace(req, env, ctx, { 142 | serviceName: 'basic-worker-tracing', 143 | collector: { 144 | url: 'http://localhost:4318/v1/traces', 145 | } 146 | }); 147 | ``` 148 | 149 | ## Support 150 | 151 | ### Cloudflare Workers 152 | 153 | This library will work out of the box for Workers but see [limitations]() for the current limitations. 154 | 155 | ## Limitations 156 | 157 | ### Cloudflare Workers 158 | 159 | There are a few limitations when using with Cloudflare Workers today, these include: 160 | - Env is not currently patchable, this means you'd need to do like `span.trace('kv:get', () => env.KV.get('abc'))` over just doing `env.KV.get('abc')` 161 | - Tracing cannot automatically resume tracing between services right now, see the [service binding example](https://github.com/BlobDevelopment/workers-tracing/tree/main/examples/service-binding) for how to do it today 162 | 163 | ## Future 164 | 165 | There are a bunch of things planned for v1 including: 166 | - Patching env (optional thing) - this will allow you to do `env.KV.get()` like normal and have tracing automatically. No need to wrap it in a trace. 167 | - Span builder - Just a nice builder pattern for the trace (credit to [repeat.dev](https://repeat.dev/) for that idea). 168 | 169 | I'd also like to make sure that Deno is supported. If you'd like to test this and fix it (or just modify the README and add tests) then please do PR :) 170 | 171 | Outside of this lib, I want to have a related project of putting all tracing components (Sender, Collector and UI) all on Cloudflare (Workers, Workers/R2 and Pages). If this interests you, let me know! 172 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs/promises'; 2 | import { argv, exit } from 'process'; 3 | import esbuild from 'esbuild'; 4 | import { replace } from 'esbuild-plugin-replace'; 5 | 6 | const format = argv.length >= 3 ? argv[2] : 'esm'; 7 | 8 | const version = JSON.parse(await readFile('package.json'), { encoding: 'utf8' }).version; 9 | 10 | async function build() { 11 | console.log(`Building Workers Tracing (in ${format} format) - Version: ${version}`); 12 | 13 | const result = await esbuild.build({ 14 | entryPoints: ['src/index.ts'], 15 | format, 16 | platform: 'neutral', 17 | bundle: true, 18 | // minify: true, 19 | sourcemap: true, 20 | outfile: `dist/workers-tracing.${format === 'esm' ? 'mjs' : 'cjs'}`, 21 | plugins: [ 22 | replace({ 23 | '__VERSION__': `${version}`, 24 | }), 25 | ], 26 | }); 27 | 28 | if (result.errors.length > 0) { 29 | console.error(result.errors.map((msg) => `${msg.id} [${msg.pluginName}]: ${msg.text} - ${msg.detail}`).join('\n')); 30 | exit(1); 31 | } else if (result.warnings.length > 0) { 32 | console.warn(result.warnings.map((msg) => `${msg.id} [${msg.pluginName}]: ${msg.text} - ${msg.detail}`).join('\n')); 33 | } else { 34 | console.log('Built successfully!'); 35 | } 36 | } 37 | 38 | build(); 39 | -------------------------------------------------------------------------------- /examples/basic/basic.js: -------------------------------------------------------------------------------- 1 | import { createTrace, SPAN_NAME, ATTRIBUTE_NAME } from 'workers-tracing'; 2 | 3 | export default { 4 | async fetch(req, env, ctx) { 5 | const trace = createTrace(req, env, ctx, { 6 | serviceName: 'basic-worker-tracing', 7 | collector: { 8 | url: 'http://localhost:4318/v1/traces', 9 | }, 10 | }); 11 | 12 | return this.handleRequest(req, env, trace); 13 | }, 14 | 15 | async handleRequest(req, env, trace) { 16 | const { pathname } = new URL(req.url); 17 | const span = trace.startSpan('handleRequest', { attributes: { path: pathname } }); 18 | 19 | await env.KV.put('abc', 'def'); 20 | 21 | // .trace will return the value from the passed function 22 | // In this case, it'll return the KV value 23 | const val = await trace.trace(SPAN_NAME.KV_GET, 24 | () => env.KV.get('abc'), 25 | { attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc '} }, 26 | ); 27 | span.addEvent({ name: 'KV lookup', timestamp: Date.now(), attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc' } }); 28 | 29 | span.end(); 30 | await trace.send(); 31 | return new Response(val); 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /examples/basic/basic.ts: -------------------------------------------------------------------------------- 1 | import { createTrace, Trace, SPAN_NAME, ATTRIBUTE_NAME } from 'workers-tracing'; 2 | 3 | interface Env { 4 | KV: KVNamespace; 5 | } 6 | 7 | export default { 8 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 9 | const trace = createTrace(req, env, ctx, { 10 | serviceName: 'basic-worker-tracing', 11 | collector: { 12 | url: 'http://localhost:4318/v1/traces', 13 | }, 14 | }); 15 | 16 | return this.handleRequest(req, env, trace); 17 | }, 18 | 19 | async handleRequest(req: Request, env: Env, trace: Trace) { 20 | const { pathname } = new URL(req.url); 21 | const span = trace.startSpan('handleRequest', { attributes: { path: pathname } }); 22 | 23 | await env.KV.put('abc', 'def'); 24 | 25 | // .trace will return the value from the passed function 26 | // In this case, it'll return the KV value 27 | const val = await trace.trace(SPAN_NAME.KV_GET, 28 | () => env.KV.get('abc'), 29 | { attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc '} }, 30 | ); 31 | span.addEvent({ name: 'KV lookup', timestamp: Date.now(), attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc' } }); 32 | 33 | span.end(); 34 | await trace.send(); 35 | return new Response(val); 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /examples/basic/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "basic", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "workers-tracing": "^0.1.1" 13 | } 14 | }, 15 | "node_modules/workers-tracing": { 16 | "version": "0.1.1", 17 | "resolved": "https://registry.npmjs.org/workers-tracing/-/workers-tracing-0.1.1.tgz", 18 | "integrity": "sha512-d6qnJFldJBzUnK9PiDpUnJB2oIh5kazADDC7YKxFyMC4EHR972jDMJsQalQL6KFD14grFN3Q5n6XmFLpAitKpA==", 19 | "dev": true 20 | } 21 | }, 22 | "dependencies": { 23 | "workers-tracing": { 24 | "version": "0.1.1", 25 | "resolved": "https://registry.npmjs.org/workers-tracing/-/workers-tracing-0.1.1.tgz", 26 | "integrity": "sha512-d6qnJFldJBzUnK9PiDpUnJB2oIh5kazADDC7YKxFyMC4EHR972jDMJsQalQL6KFD14grFN3Q5n6XmFLpAitKpA==", 27 | "dev": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "main": "basic.js", 5 | "private": true, 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "wrangler dev --local basic.ts", 9 | "js": "wrangler dev --local basic.js", 10 | "ts": "wrangler dev --local basic.ts" 11 | }, 12 | "devDependencies": { 13 | "workers-tracing": "^0.1.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/basic/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "basic" 2 | compatibility_date = "2022-12-27" 3 | main = "basic.ts" 4 | 5 | [[kv_namespaces]] 6 | binding = "KV" 7 | id = "a" 8 | preview_id = "a" 9 | -------------------------------------------------------------------------------- /examples/service-binding/index.ts: -------------------------------------------------------------------------------- 1 | import { createTrace, SPAN_NAME, Trace } from 'workers-tracing'; 2 | 3 | interface Env { 4 | AUTH_TOKEN: string; 5 | SERVICE: Fetcher; 6 | } 7 | 8 | export default { 9 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 10 | const trace = createTrace(req, env, ctx, { 11 | serviceName: 'service-binding-example', 12 | collector: { 13 | url: 'http://localhost:4318/v1/traces', 14 | headers: { 15 | Authorization: `Bearer ${env.AUTH_TOKEN}`, 16 | }, 17 | }, 18 | }); 19 | 20 | return this.handleRequest(req, env, trace); 21 | }, 22 | 23 | async handleRequest(req: Request, env: Env, trace: Trace) { 24 | const { pathname } = new URL(req.url); 25 | const span = trace.startSpan('handleRequest', { attributes: { path: pathname } }); 26 | 27 | const serviceBindingSpan = span.startSpan(SPAN_NAME.SERVICE_FETCH, { attributes: { service: 'basic' } }); 28 | const res = await env.SERVICE.fetch('https://service/test', { 29 | headers: { 'x-trace-id': `${serviceBindingSpan.getContext().traceId}:${serviceBindingSpan.getContext().spanId}` }, 30 | }); 31 | serviceBindingSpan.end(); 32 | 33 | span.end(); 34 | await trace.send(); 35 | return res; 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /examples/service-binding/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "basic", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "workers-tracing": "^0.1.1" 13 | } 14 | }, 15 | "node_modules/workers-tracing": { 16 | "version": "0.1.1", 17 | "resolved": "https://registry.npmjs.org/workers-tracing/-/workers-tracing-0.1.1.tgz", 18 | "integrity": "sha512-d6qnJFldJBzUnK9PiDpUnJB2oIh5kazADDC7YKxFyMC4EHR972jDMJsQalQL6KFD14grFN3Q5n6XmFLpAitKpA==", 19 | "dev": true 20 | } 21 | }, 22 | "dependencies": { 23 | "workers-tracing": { 24 | "version": "0.1.1", 25 | "resolved": "https://registry.npmjs.org/workers-tracing/-/workers-tracing-0.1.1.tgz", 26 | "integrity": "sha512-d6qnJFldJBzUnK9PiDpUnJB2oIh5kazADDC7YKxFyMC4EHR972jDMJsQalQL6KFD14grFN3Q5n6XmFLpAitKpA==", 27 | "dev": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/service-binding/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "basic", 3 | "version": "1.0.0", 4 | "main": "basic.js", 5 | "private": true, 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "wrangler dev --local basic.ts", 9 | "js": "wrangler dev --local basic.js", 10 | "ts": "wrangler dev --local basic.ts" 11 | }, 12 | "devDependencies": { 13 | "workers-tracing": "^0.1.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/service-binding/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "service-binding" 2 | compatibility_date = "2022-12-27" 3 | main = "index.ts" 4 | 5 | [[services]] 6 | binding = "SERVICE" 7 | service = "basic" 8 | environment = "production" 9 | -------------------------------------------------------------------------------- /examples/span-builder/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "span-builder", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "wrangler dev --local" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@cloudflare/workers-types": "^4.20221111.1", 14 | "wrangler": "^2.6.2" 15 | }, 16 | "dependencies": { 17 | "workers-tracing": "^0.1.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /examples/span-builder/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/index'; 2 | import { createTrace } from 'src/trace'; 3 | 4 | export interface Env { 5 | KV: KVNamespace; 6 | } 7 | 8 | export default { 9 | async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise { 10 | const trace = createTrace(req, env, ctx, { 11 | serviceName: 'span-builder', 12 | collector: { 13 | url: 'http://localhost:4318/v1/traces', 14 | }, 15 | }); 16 | 17 | const forLoopSpan = trace.startSpan('for_loop'); 18 | for (let i = 0; i < 10; i++) { 19 | const span = trace.buildSpan(SPAN_NAME.KV_GET) 20 | .addAttribute('Index', i) 21 | .addAttribute(ATTRIBUTE_NAME.KV_KEY, `id:${i}`) 22 | .addLink(forLoopSpan); 23 | 24 | await env.KV.put(`id:${i}`, JSON.stringify({ idx: i })); 25 | await env.KV.get(`id:${i}`); 26 | 27 | span.end(); 28 | } 29 | forLoopSpan.end(); 30 | 31 | trace.send(); 32 | 33 | return new Response('ok'); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /examples/span-builder/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "span-builder" 2 | main = "src/index.ts" 3 | compatibility_date = "2023-01-02" 4 | 5 | [[kv_namespaces]] 6 | binding = "KV" 7 | id = "a" 8 | preview_id = "a" 9 | -------------------------------------------------------------------------------- /examples/zipkin-basic/index.ts: -------------------------------------------------------------------------------- 1 | import { createTrace, SPAN_NAME, Trace, traceFn, ZipkinTransformer } from 'workers-tracing'; 2 | 3 | interface Env { 4 | AUTH_TOKEN: string; 5 | KV: KVNamespace; 6 | } 7 | 8 | export default { 9 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 10 | const trace = createTrace(req, env, ctx, { 11 | serviceName: 'basic-worker-tracing', 12 | collector: { 13 | url: 'http://localhost:9411/api/v2/spans', 14 | transformer: new ZipkinTransformer(), 15 | }, 16 | }); 17 | 18 | await traceFn(trace, SPAN_NAME.FETCH, () => fetch('https://example.com/')); 19 | 20 | return this.handleRequest(req, env, trace); 21 | }, 22 | 23 | async handleRequest(req: Request, env: Env, trace: Trace) { 24 | const { pathname } = new URL(req.url); 25 | const span = trace.startSpan('handleRequest', { attributes: { path: pathname } }); 26 | 27 | await env.KV.put('abc', 'def'); 28 | 29 | const val = await traceFn(span, SPAN_NAME.KV_GET, () => env.KV.get('abc'), { attributes: { key: 'abc '} }); 30 | span.addEvent({ name: 'Fetch done', timestamp: Date.now() }); 31 | 32 | span.end(); 33 | await trace.send(); 34 | return new Response(val); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /examples/zipkin-basic/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zipkin-basic", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "zipkin-basic", 9 | "version": "1.0.0", 10 | "license": "ISC", 11 | "devDependencies": { 12 | "workers-tracing": "^0.1.1" 13 | } 14 | }, 15 | "node_modules/workers-tracing": { 16 | "version": "0.1.1", 17 | "resolved": "https://registry.npmjs.org/workers-tracing/-/workers-tracing-0.1.1.tgz", 18 | "integrity": "sha512-d6qnJFldJBzUnK9PiDpUnJB2oIh5kazADDC7YKxFyMC4EHR972jDMJsQalQL6KFD14grFN3Q5n6XmFLpAitKpA==", 19 | "dev": true 20 | } 21 | }, 22 | "dependencies": { 23 | "workers-tracing": { 24 | "version": "0.1.1", 25 | "resolved": "https://registry.npmjs.org/workers-tracing/-/workers-tracing-0.1.1.tgz", 26 | "integrity": "sha512-d6qnJFldJBzUnK9PiDpUnJB2oIh5kazADDC7YKxFyMC4EHR972jDMJsQalQL6KFD14grFN3Q5n6XmFLpAitKpA==", 27 | "dev": true 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /examples/zipkin-basic/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zipkin-basic", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "workers-tracing": "^0.1.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /examples/zipkin-basic/wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "zipkin-basic" 2 | compatibility_date = "2022-12-27" 3 | main = "index.ts" 4 | 5 | [[kv_namespaces]] 6 | binding = "KV" 7 | id = "a" 8 | preview_id = "a" 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workers-tracing", 3 | "version": "0.1.3", 4 | "description": "Enable tracing within Workers with this simple package! Simply trace and send to a collector with a compatible export format", 5 | "author": "Daniel Walsh (@WalshyDev)", 6 | "license": "MIT", 7 | "type": "module", 8 | "main": "dist/workers-tracing.cjs", 9 | "module": "dist/workers-tracing.mjs", 10 | "types": "dist/index.d.ts", 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/BlobDevelopment/workers-tracing.git" 14 | }, 15 | "exports": { 16 | ".": { 17 | "require": "./dist/workers-tracing.cjs", 18 | "import": "./dist/workers-tracing.mjs", 19 | "types": "./dist/index.d.ts" 20 | } 21 | }, 22 | "files": [ 23 | "dist/", 24 | "README.md", 25 | "LICENSE" 26 | ], 27 | "keywords": [ 28 | "workers", 29 | "tracing", 30 | "cloudflare", 31 | "cloudflare workers", 32 | "opentelemetry", 33 | "zipkin", 34 | "jaeger" 35 | ], 36 | "scripts": { 37 | "build": "npm run build:esm && npm run build:cjs && npm run build:types", 38 | "build:esm": "node build.js esm", 39 | "build:cjs": "node build.js cjs", 40 | "build:types": "tsc -p tsconfig.emit.json", 41 | "record-sizes": "rm -rf dist && npm run build && gzip dist/workers-tracing.mjs dist/workers-tracing.cjs && npm run build && ls -lah dist", 42 | "test": "vitest", 43 | "test:ci": "vitest run --watch false --no-threads", 44 | "lint": "eslint --ext js,ts src test", 45 | "lint:fix": "eslint --fix --ext js,ts src test", 46 | "changeset": "npx changeset", 47 | "publish": "npm run build && npx changeset publish" 48 | }, 49 | "devDependencies": { 50 | "@changesets/changelog-github": "^0.4.8", 51 | "@changesets/cli": "^2.26.0", 52 | "@cloudflare/workers-types": "^3.18.0", 53 | "@typescript-eslint/eslint-plugin": "^5.47.1", 54 | "@typescript-eslint/parser": "^5.47.1", 55 | "esbuild-plugin-replace": "^1.3.0", 56 | "eslint": "^8.30.0", 57 | "eslint-plugin-import": "^2.26.0", 58 | "vitest": "^0.26.2", 59 | "wrangler": "^2.6.2" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/builder.ts: -------------------------------------------------------------------------------- 1 | import { Span, StatusCode } from './tracing'; 2 | import type { Attribute, Attributes, SpanContext } from './types'; 3 | 4 | export class SpanBuilder { 5 | 6 | #span: Span; 7 | 8 | constructor(parent: Span, name: string) { 9 | this.#span = parent.startSpan(name); 10 | } 11 | 12 | addAttribute(key: string, value: Attribute): SpanBuilder { 13 | this.#span.getData().attributes[key] = value; 14 | return this; 15 | } 16 | 17 | removeAttribute(key: string): SpanBuilder { 18 | delete this.#span.getData().attributes[key]; 19 | return this; 20 | } 21 | 22 | setStatus(code: StatusCode, message?: string): SpanBuilder { 23 | this.#span.setStatus(code, message); 24 | return this; 25 | } 26 | 27 | addEvent(name: string, attributes?: Attributes): SpanBuilder { 28 | this.#span.addEvent({ name, timestamp: Date.now(), attributes }); 29 | return this; 30 | } 31 | 32 | addLink(ctx: SpanContext | Span, attributes?: Attributes): SpanBuilder { 33 | if (ctx instanceof Span) { 34 | ctx = ctx.getContext(); 35 | } 36 | 37 | this.#span.getData().links.push({ context: ctx, attributes: attributes ?? {} }); 38 | return this; 39 | } 40 | 41 | end(timestamp?: number) { 42 | this.#span.end(timestamp); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './trace'; 2 | export * from './tracing'; 3 | export * from './transformers/otlp'; 4 | export * from './transformers/zipkin'; 5 | export * from './utils/constants'; 6 | -------------------------------------------------------------------------------- /src/trace.ts: -------------------------------------------------------------------------------- 1 | import { Span, Trace } from './tracing'; 2 | import { CfWithTrace, SpanContext, SpanCreationOptions, TracedFn, TracerOptions } from './types'; 3 | 4 | export function createTrace( 5 | req: Request, 6 | env: unknown, 7 | ctx: ExecutionContext, 8 | tracerOptions: TracerOptions, 9 | spanOptions?: SpanCreationOptions, 10 | ): Trace { 11 | // This is ugly 12 | // TODO: Fix this - https://www.w3.org/TR/trace-context/#traceparent-header 13 | // https://zipkin.io/pages/architecture.html - https://github.com/openzipkin/b3-propagation#overall-process 14 | // This parent context will allow properly tracing across services (and other Workers) 15 | let parentContext: SpanContext | undefined; 16 | if ((req.cf as CfWithTrace)?.traceContext) { 17 | parentContext = (req.cf as CfWithTrace)?.traceContext; 18 | } 19 | if (req.headers.get('x-trace-id')) { 20 | const ids = req.headers.get('x-trace-id')?.split(':', 2); 21 | if (ids?.length === 2) { 22 | parentContext = { traceId: ids[0], spanId: ids[1] }; 23 | } 24 | } 25 | 26 | const trace = new Trace(ctx, { 27 | traceContext: parentContext, 28 | ...tracerOptions, 29 | }, spanOptions); 30 | 31 | return trace; 32 | } 33 | 34 | export function traceFn( 35 | parent: Span, 36 | name: string, 37 | fn: TracedFn, 38 | opts?: SpanCreationOptions, 39 | ): T { 40 | const span = parent.startSpan(name, opts); 41 | 42 | const value = fn(); 43 | 44 | if (value instanceof Promise) { 45 | value.finally(() => span.end()); 46 | return value; 47 | } 48 | 49 | span.end(); 50 | return value; 51 | } 52 | -------------------------------------------------------------------------------- /src/tracing.ts: -------------------------------------------------------------------------------- 1 | import { OtlpTransformer } from './transformers/otlp'; 2 | import { ATTRIBUTE_NAME } from './utils/constants'; 3 | import { generateSpanId, generateTraceId } from './utils/rand'; 4 | import { traceFn } from './trace'; 5 | import { SpanBuilder } from './builder'; 6 | import type { 7 | Attributes, 8 | SpanContext, 9 | SpanCreationOptions, 10 | SpanData, 11 | SpanEvent, 12 | TracedFn, 13 | TracerOptions, 14 | } from './types'; 15 | 16 | export enum StatusCode { 17 | UNSET = 0, 18 | OK = 1, 19 | ERROR = 2, 20 | } 21 | 22 | export function getDefaultAttributes(opts: TracerOptions): Attributes { 23 | return { 24 | [ATTRIBUTE_NAME.SERVICE_NAME]: opts.serviceName, 25 | [ATTRIBUTE_NAME.SDK_NAME]: 'workers-tracing', 26 | [ATTRIBUTE_NAME.SDK_LANG]: 'javascript', 27 | [ATTRIBUTE_NAME.SDK_VERSION]: '__VERSION__', 28 | [ATTRIBUTE_NAME.RUNTIME_NAME]: 29 | typeof navigator !== 'undefined' && navigator.userAgent // Cloudflare-Workers 30 | ? navigator.userAgent 31 | : 'Unknown', 32 | }; 33 | } 34 | 35 | export class Span { 36 | 37 | #span: SpanData; 38 | #childSpans: Span[]; 39 | 40 | constructor(traceId: string, name: string, spanOptions?: SpanCreationOptions) { 41 | this.#span = { 42 | traceId: traceId, 43 | name, 44 | id: generateSpanId(), 45 | parentId: spanOptions?.parentId, 46 | timestamp: spanOptions?.timestamp ?? Date.now(), 47 | duration: spanOptions?.duration ?? 0, 48 | attributes: spanOptions?.attributes ?? {}, 49 | status: spanOptions?.status ?? { code: StatusCode.UNSET }, 50 | events: spanOptions?.events ?? [], 51 | links: spanOptions?.links ?? [], 52 | }; 53 | this.#childSpans = []; 54 | } 55 | 56 | getTraceId() { 57 | return this.#span.traceId; 58 | } 59 | 60 | getSpanId() { 61 | return this.#span.id; 62 | } 63 | 64 | getData() { 65 | return this.#span; 66 | } 67 | 68 | getChildSpans() { 69 | return this.#childSpans; 70 | } 71 | 72 | getContext(): SpanContext { 73 | return { traceId: this.#span.traceId, spanId: this.#span.id }; 74 | } 75 | 76 | startSpan(name: string, spanOptions?: SpanCreationOptions): Span { 77 | const span = new Span(this.#span.traceId, name, spanOptions); 78 | span.#span.parentId = this.getSpanId(); 79 | this.#childSpans.push(span); 80 | 81 | return span; 82 | } 83 | 84 | trace(name: string, fn: TracedFn, opts?: SpanCreationOptions): T { 85 | return traceFn(this, name, fn, opts); 86 | } 87 | 88 | buildSpan(name: string) { 89 | return new SpanBuilder(this, name); 90 | } 91 | 92 | setStatus(status: StatusCode, message?: string) { 93 | this.#span.status = { code: status, message }; 94 | } 95 | 96 | addEvent(event: SpanEvent) { 97 | this.#span.events.push(event); 98 | } 99 | 100 | end(timestamp?: number) { 101 | this.#span.duration = (timestamp ?? Date.now()) - this.#span.timestamp; 102 | } 103 | } 104 | 105 | export class Trace extends Span { 106 | 107 | #ctx: ExecutionContext; 108 | #tracerOptions: TracerOptions; 109 | 110 | constructor( 111 | ctx: ExecutionContext, 112 | tracerOptions: TracerOptions, 113 | spanOptions?: SpanCreationOptions, 114 | ) { 115 | super( 116 | tracerOptions.traceContext?.traceId ?? generateTraceId(), 117 | 'Request (fetch event)', 118 | { 119 | parentId: tracerOptions.traceContext?.spanId, 120 | ...spanOptions, 121 | }, 122 | ); 123 | this.#ctx = ctx; 124 | this.#tracerOptions = tracerOptions; 125 | 126 | if (!this.#tracerOptions.resource) { 127 | this.#tracerOptions.resource = { attributes: getDefaultAttributes(tracerOptions) }; 128 | } else if (!this.#tracerOptions.resource.attributes) { 129 | this.#tracerOptions.resource.attributes = getDefaultAttributes(tracerOptions); 130 | } else { 131 | this.#tracerOptions.resource.attributes = { 132 | ...getDefaultAttributes(tracerOptions), 133 | ...this.#tracerOptions.resource.attributes, 134 | }; 135 | } 136 | } 137 | 138 | /** 139 | * @deprecated Use #getChildSpans 140 | */ 141 | getSpans() { 142 | return this.getChildSpans(); 143 | } 144 | 145 | getTracerOptions() { 146 | return this.#tracerOptions; 147 | } 148 | 149 | async send() { 150 | // We need to end the trace here 151 | this.end(); 152 | 153 | const headers = new Headers(this.#tracerOptions.collector.headers); 154 | if (headers.has('content-type')) { 155 | // We want to override this since we will pass JSON 156 | headers.delete('content-type'); 157 | } 158 | headers.append('content-type', 'application/json'); 159 | 160 | // TODO: Properly pass trace context down and update the tests 161 | if (!headers.has('x-trace-id')) { 162 | headers.append('x-trace-id', this.getTraceId()); 163 | } 164 | 165 | let body; 166 | if (this.#tracerOptions.collector.transformer) { 167 | body = this.#tracerOptions.collector.transformer.transform(this); 168 | } else { 169 | body = new OtlpTransformer().transform(this); 170 | } 171 | 172 | const bodyStr = JSON.stringify(body); 173 | 174 | // If we're in Miniflare, we wait for the fetch to complete. 175 | // This is mainly for tests but helpful for local testing too 176 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 177 | // @ts-ignore 178 | if (globalThis.MINIFLARE) { 179 | console.log(`Sending to: ${this.#tracerOptions.collector.url}`); 180 | console.log(headers); 181 | console.log(body); 182 | const res = await fetch(this.#tracerOptions.collector.url, { 183 | method: 'POST', 184 | headers, 185 | body: bodyStr, 186 | }); 187 | 188 | const txt = await res.text(); 189 | console.log(txt); 190 | } else { 191 | this.#ctx.waitUntil(fetch(this.#tracerOptions.collector.url, { 192 | method: 'POST', 193 | headers, 194 | body: bodyStr, 195 | })); 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/transformers/otlp.ts: -------------------------------------------------------------------------------- 1 | import { Span, Trace } from 'src/tracing'; 2 | import { TraceTransformer } from './transformer'; 3 | import type { Attribute, Attributes } from 'src/types'; 4 | 5 | export interface OtlpJson { 6 | resourceSpans: OtlpResourceSpan[]; 7 | } 8 | 9 | export interface OtlpResourceSpan { 10 | resource: OtlpResource; 11 | scopeSpans: OtlpScopeSpan[]; 12 | } 13 | 14 | export interface OtlpResource { 15 | attributes: OtlpAttribute[]; 16 | } 17 | 18 | export interface OtlpScopeSpan { 19 | scope: OtlpScope; 20 | spans: OtlpSpan[]; 21 | } 22 | 23 | export interface OtlpScope { 24 | name: string; 25 | } 26 | 27 | export interface OtlpSpan { 28 | traceId: string; 29 | spanId: string; 30 | parentSpanId?: string; 31 | name: string; 32 | kind: number; 33 | startTimeUnixNano: number; 34 | endTimeUnixNano: number; 35 | attributes: OtlpAttribute[]; 36 | events: OtlpEvent[]; 37 | status: OtlpStatus; 38 | links: OtlpLink[]; 39 | } 40 | 41 | export interface OtlpStatus { 42 | code: number; 43 | } 44 | 45 | export interface OtlpAttribute { 46 | key: string; 47 | value: OtlpValue; 48 | } 49 | 50 | export interface OtlpValue { 51 | stringValue?: string; 52 | intValue?: number; 53 | boolValue?: boolean; 54 | doubleValue?: number; 55 | 56 | arrayValue?: { values: OtlpValue[] }; 57 | } 58 | 59 | export interface OtlpEvent { 60 | name: string; 61 | timeUnixNano: number; 62 | attributes: OtlpAttribute[]; 63 | } 64 | 65 | export interface OtlpLink { 66 | traceId: string; 67 | spanId: string; 68 | attributes: OtlpAttribute[]; 69 | } 70 | 71 | export type TransformValue = (value: Attribute) => OtlpValue | null; 72 | 73 | export class OtlpTransformer extends TraceTransformer { 74 | 75 | transform(trace: Trace): OtlpJson { 76 | return { 77 | resourceSpans: [ 78 | { 79 | resource: { 80 | attributes: this.transformAttributes( 81 | { 82 | ...trace.getTracerOptions().resource?.attributes, 83 | }, 84 | ), 85 | }, 86 | 87 | scopeSpans: [ 88 | { 89 | scope: { 90 | name: trace.getTracerOptions().serviceName, 91 | }, 92 | spans: this.collectSpans(trace).map((span) => this.transformSpan(span)), 93 | }, 94 | ], 95 | }, 96 | ], 97 | }; 98 | } 99 | 100 | transformAttributes(attributes: Attributes): OtlpAttribute[] { 101 | const transformed: OtlpAttribute[] = []; 102 | 103 | const transformValue: TransformValue = (value: Attribute) => { 104 | if (Array.isArray(value)) { 105 | const values: OtlpValue[] = []; 106 | 107 | for (const val of value) { 108 | const transformed = transformValue(val); 109 | if (transformed) { 110 | values.push(transformed); 111 | } 112 | } 113 | 114 | return { arrayValue: { values } }; 115 | } else { 116 | if (typeof value === 'string') { 117 | return { stringValue: value }; 118 | } else if (typeof value === 'number') { 119 | if (Number.isInteger(value)) { 120 | return { intValue: value }; 121 | } else { 122 | return { doubleValue: value }; 123 | } 124 | } else if (typeof value === 'boolean') { 125 | return { boolValue: value }; 126 | } else { 127 | console.error('Unsupported value type: ' + typeof value); 128 | return null; 129 | } 130 | } 131 | }; 132 | 133 | for (const [key, value] of Object.entries(attributes)) { 134 | const transformedValue = transformValue(value); 135 | if (transformedValue === null) continue; // Skip invalid values 136 | 137 | transformed.push({ 138 | key: key, 139 | value: transformedValue, 140 | }); 141 | } 142 | 143 | return transformed; 144 | } 145 | 146 | transformSpan(span: Span): OtlpSpan { 147 | const data = span.getData(); 148 | 149 | return { 150 | traceId: data.traceId, 151 | spanId: data.id, 152 | parentSpanId: data.parentId, 153 | name: data.name, 154 | kind: 0, // TODO: Implement kind (https://opentelemetry.io/docs/reference/specification/trace/api/#spankind) 155 | startTimeUnixNano: data.timestamp * 1e6, 156 | endTimeUnixNano: (data.timestamp + data.duration) * 1e6, 157 | attributes: this.transformAttributes(data.attributes), 158 | events: data.events.map((event) => ( 159 | { 160 | name: event.name, 161 | timeUnixNano: event.timestamp * 1e6, 162 | attributes: this.transformAttributes(event.attributes || {}), 163 | } 164 | )), 165 | status: data.status, 166 | links: data.links.map((link) => ({ 167 | traceId: link.context.traceId, 168 | spanId: link.context.spanId, 169 | attributes: this.transformAttributes(link.attributes), 170 | })), 171 | }; 172 | } 173 | 174 | collectSpans(span: Span): Span[] { 175 | const spans = []; 176 | 177 | spans.push(span); 178 | 179 | // Go through children and collect them all 180 | for (const childSpan of span.getChildSpans()) { 181 | spans.push(...this.collectSpans(childSpan)); 182 | } 183 | 184 | return spans; 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/transformers/transformer.ts: -------------------------------------------------------------------------------- 1 | import { Span, Trace } from 'src/tracing'; 2 | 3 | export class TraceTransformer { 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 6 | transform(trace: Trace): object { 7 | throw new Error('Transformer has not implemented `transform()` function'); 8 | } 9 | 10 | collectSpans(span: Span): Span[] { 11 | const spans = []; 12 | 13 | spans.push(span); 14 | // Go through children and collect them all 15 | for (const childSpan of span.getChildSpans()) { 16 | spans.push(...this.collectSpans(childSpan)); 17 | } 18 | 19 | return spans; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/transformers/zipkin.ts: -------------------------------------------------------------------------------- 1 | import { Trace } from 'src/tracing'; 2 | import { TraceTransformer } from './transformer'; 3 | 4 | export type ZipkinJson = ZipkinSpan[]; 5 | 6 | export interface ZipkinSpan { 7 | traceId: string; 8 | name: string; 9 | id: string; // Span ID 10 | parentId?: string; 11 | timestamp: number; // Microseconds 12 | duration: number; // Microseconds 13 | localEndpoint: ZipkinEndpoint; 14 | tags?: ZipkinTags; 15 | annotations?: ZipkinAnnotation[]; 16 | } 17 | 18 | export interface ZipkinEndpoint { 19 | serviceName: string; 20 | } 21 | 22 | export interface ZipkinTags { 23 | [key: string]: string; 24 | } 25 | 26 | export interface ZipkinAnnotation { 27 | timestamp: number; // Microseconds 28 | value: string; 29 | } 30 | 31 | export class ZipkinTransformer extends TraceTransformer { 32 | 33 | transform(trace: Trace): ZipkinJson { 34 | const spans: ZipkinJson = []; 35 | 36 | for (const span of this.collectSpans(trace)) { 37 | const data = span.getData(); 38 | 39 | if (span instanceof Trace) { 40 | // In the case of Zipkin, we want to put resource attributes on the span 41 | data.attributes = { ...span.getTracerOptions().resource?.attributes, ...data.attributes }; 42 | } 43 | 44 | const tags: ZipkinTags = {}; 45 | for (const [key, value] of Object.entries(data.attributes)) { 46 | tags[key] = String(value); 47 | } 48 | 49 | spans.push({ 50 | name: data.name, 51 | traceId: data.traceId, 52 | id: data.id, 53 | parentId: data.parentId, 54 | timestamp: data.timestamp * 1e3, 55 | duration: data.duration * 1e3, 56 | localEndpoint: { 57 | serviceName: trace.getTracerOptions().serviceName, 58 | }, 59 | tags, 60 | annotations: data.events.map((event) => ({ timestamp: event.timestamp * 1e3, value: event.name })), 61 | }); 62 | } 63 | 64 | return spans; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import { StatusCode } from './tracing'; 2 | import { TraceTransformer } from './transformers/transformer'; 3 | 4 | export interface TraceData { 5 | // 8 bit field currently only used to indicate sampling 6 | // https://www.w3.org/TR/trace-context/#trace-flags 7 | flags?: number; 8 | 9 | spans: SpanData[]; 10 | } 11 | 12 | export interface SpanContext { 13 | // Globally unique ID for this specific trace event 14 | // 16 bytes, should be random rather than computed. 15 | // https://www.w3.org/TR/trace-context/#trace-id 16 | // https://www.w3.org/TR/trace-context/#considerations-for-trace-id-field-generation 17 | traceId: string; 18 | 19 | // Globally unique ID for this span 20 | // 8 bytes, should be random rather than computed 21 | spanId: string; 22 | } 23 | 24 | export interface SpanData { 25 | // Globally unique ID for this specific trace event 26 | // 16 bytes, should be random rather than computed. 27 | // https://www.w3.org/TR/trace-context/#trace-id 28 | // https://www.w3.org/TR/trace-context/#considerations-for-trace-id-field-generation 29 | traceId: string; 30 | 31 | // Span ID of the parent or undefined if there is no parent present. 32 | // https://www.w3.org/TR/trace-context/#parent-id 33 | parentId?: string; 34 | 35 | // Name of the span 36 | name: string; 37 | 38 | // Globally unique ID for this span 39 | // 8 bytes, should be random rather than computed 40 | id: string; 41 | 42 | // The creation time of the span, epoch unix timestamp 43 | // https://opentelemetry.io/docs/reference/specification/trace/api/#timestamp 44 | timestamp: number; 45 | 46 | // The duration of the span, this is the elapsed time. 47 | // https://opentelemetry.io/docs/reference/specification/trace/api/#duration 48 | duration: number; 49 | 50 | // Attributes (or tags) attached to the span. These can be used to attach details like URL path, status code, etc. 51 | // https://opentelemetry.io/docs/reference/specification/common/#attribute 52 | attributes: Attributes; 53 | 54 | // https://opentelemetry.io/docs/reference/specification/trace/api/#set-status 55 | status: Status; 56 | 57 | // Events 58 | events: SpanEvent[]; 59 | 60 | // Links to spans in this or other traces 61 | links: SpanLink[]; 62 | } 63 | 64 | export interface SpanCreationOptions { 65 | // Span ID of the parent or undefined if there is no parent present. 66 | // https://www.w3.org/TR/trace-context/#parent-id 67 | parentId?: string; 68 | 69 | // The creation time of the span, epoch unix timestamp 70 | // https://opentelemetry.io/docs/reference/specification/trace/api/#timestamp 71 | timestamp?: number; 72 | 73 | // The duration of the span, this is the elapsed time. 74 | // https://opentelemetry.io/docs/reference/specification/trace/api/#duration 75 | duration?: number; 76 | 77 | // Attributes (or tags) attached to the span. These can be used to attach details like URL path, status code, etc. 78 | // https://opentelemetry.io/docs/reference/specification/common/#attribute 79 | attributes?: Attributes; 80 | 81 | // https://opentelemetry.io/docs/reference/specification/trace/api/#set-status 82 | status?: Status; 83 | 84 | // Events 85 | events?: SpanEvent[]; 86 | 87 | // Links to spans in this or other traces 88 | links?: SpanLink[]; 89 | } 90 | 91 | export interface Attributes { 92 | [key: string]: Attribute; 93 | } 94 | 95 | export type Attribute = string | number | boolean | string[] | number[] | boolean[]; 96 | 97 | export interface Status { 98 | code: StatusCode; 99 | message?: string; 100 | } 101 | 102 | export interface SpanEvent { 103 | // Name of the event 104 | name: string; 105 | 106 | // Time of the event, this will be when the event is added if no timestamp is provided 107 | timestamp: number; 108 | 109 | // Attributes (or tags) attached to the span. These can be used to attach details like URL path, status code, etc. 110 | // https://opentelemetry.io/docs/reference/specification/common/#attribute 111 | attributes?: Attributes; 112 | } 113 | 114 | export interface SpanLink { 115 | context: SpanContext; 116 | attributes: Attributes; 117 | } 118 | 119 | export interface TracerOptions { 120 | serviceName: string; 121 | collector: CollectorOptions; 122 | resource?: ResourceOptions; 123 | traceContext?: SpanContext; 124 | } 125 | 126 | export interface CollectorOptions { 127 | url: string; 128 | headers?: HeadersInit; 129 | transformer?: TraceTransformer; 130 | } 131 | 132 | export interface ResourceOptions { 133 | attributes?: Attributes; 134 | } 135 | 136 | export type CfWithTrace = IncomingRequestCfProperties & { 137 | traceContext?: SpanContext; 138 | } 139 | 140 | export type TracedFn = (...args: unknown[]) => T; 141 | -------------------------------------------------------------------------------- /src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const ATTRIBUTE_NAME = Object.freeze({ 2 | SERVICE_NAME: 'service.name', 3 | SERVICE_VERSION: 'service.version', 4 | 5 | OTLP_STATUS_CODE: 'otel.status_code', 6 | ERROR: 'error', 7 | 8 | SDK_NAME: 'telemetry.sdk.name', 9 | SDK_VERSION: 'telemetry.sdk.version', 10 | SDK_LANG: 'telemetry.sdk.language', 11 | 12 | RUNTIME_NAME: 'process.runtime.name', 13 | RUNTIME_VERSION: 'process.runtime.version', 14 | RUNTIME_DESCRIPTION: 'process.runtime.description', 15 | 16 | HTTP_HOST: 'http.host', 17 | HTTP_PATH: 'http.path', 18 | 19 | // Workers specific 20 | KV_KEY: 'kv.key', 21 | }); 22 | 23 | export const SPAN_NAME = Object.freeze({ 24 | // General 25 | FETCH: 'fetch', 26 | // KV 27 | KV_GET: 'kv:get', 28 | KV_GET_METADATA: 'kv:getWithMetadata', 29 | KV_LIST: 'kv:list', 30 | KV_DELETE: 'kv:delete', 31 | // Durable Object 32 | DO_FETCH: 'durable_object:fetch', 33 | DO_ALARM: 'durable_object:alarm', 34 | // Service 35 | SERVICE_FETCH: 'service:fetch', 36 | // R2 37 | R2_HEAD: 'r2:head', 38 | R2_GET: 'r2:get', 39 | R2_PUT: 'r2:put', 40 | R2_LIST: 'r2:list', 41 | R2_DELETE: 'r2:delete', 42 | // WfP 43 | DISPATCHER_GET: 'wfp:dispatcher:get', 44 | WORKER_FETCH: 'wfp:worker:fetch', 45 | // Queues 46 | QUEUE_SEND: 'queue:send', 47 | }); 48 | -------------------------------------------------------------------------------- /src/utils/rand.ts: -------------------------------------------------------------------------------- 1 | export function generateTraceId() { 2 | return generateId(16); 3 | } 4 | 5 | export function generateSpanId() { 6 | return generateId(8); 7 | } 8 | 9 | export function generateId(length: number) { 10 | const arr = new Uint32Array(length); 11 | crypto.getRandomValues(arr); 12 | 13 | return Array.from(arr, (byte) => ('0' + (byte & 0xFF).toString(16)).slice(-2)).join(''); 14 | } 15 | -------------------------------------------------------------------------------- /test/api.test.ts: -------------------------------------------------------------------------------- 1 | import { StatusCode } from 'src/tracing'; 2 | import { ATTRIBUTE_NAME } from 'src/utils/constants'; 3 | import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; 4 | import { UnstableDevWorker } from 'wrangler'; 5 | import { getTrace, requestAndGetTrace } from './utils/trace'; 6 | import { startCollector, startWorker } from './utils/worker'; 7 | 8 | let devWorker: UnstableDevWorker; 9 | let collectorWorker: UnstableDevWorker; 10 | 11 | const URL = 'http://worker/test'; 12 | 13 | describe('API', () => { 14 | beforeAll(async () => { 15 | collectorWorker = await startCollector({ port: 4318 }); 16 | }); 17 | 18 | afterEach(async () => { 19 | if (devWorker) { 20 | await devWorker.stop(); 21 | await devWorker.waitUntilExit(); 22 | } 23 | }); 24 | 25 | afterAll(async () => { 26 | if (collectorWorker) { 27 | await collectorWorker.stop(); 28 | await collectorWorker.waitUntilExit(); 29 | } 30 | }); 31 | 32 | describe('createTrace', () => { 33 | describe('Sanity checks', () => { 34 | test('Trace ID should be 32 hex chars', async () => { 35 | devWorker = await startWorker('test/scripts/api/root-span.ts'); 36 | 37 | const res = await devWorker.fetch('http://worker/test'); 38 | 39 | expect(res.status).toBe(200); 40 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 41 | 42 | const traceId = res.headers.get('x-trace-id'); 43 | if (traceId === null) { 44 | expect(traceId).not.toBeNull(); 45 | return; 46 | } 47 | const trace = await getTrace(collectorWorker, traceId); 48 | 49 | expect(trace.resourceSpans.length).toBe(1); 50 | const resourceSpan = trace.resourceSpans[0]; 51 | 52 | expect(resourceSpan.scopeSpans.length).toBe(1); 53 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('root-span'); 54 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(1); 55 | const span = resourceSpan.scopeSpans[0].spans[0]; 56 | 57 | // Sanity check trace ID 58 | expect(span.traceId.length).toBe(32); 59 | expect(span.traceId).toMatch(/[a-f0-9]{32}/); 60 | }); 61 | 62 | test('Span ID should be 16 hex chars', async () => { 63 | devWorker = await startWorker('test/scripts/api/root-span.ts'); 64 | 65 | const res = await devWorker.fetch('http://worker/test'); 66 | 67 | expect(res.status).toBe(200); 68 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 69 | 70 | const traceId = res.headers.get('x-trace-id'); 71 | if (traceId === null) { 72 | expect(traceId).not.toBeNull(); 73 | return; 74 | } 75 | const trace = await getTrace(collectorWorker, traceId); 76 | 77 | expect(trace.resourceSpans.length).toBe(1); 78 | const resourceSpan = trace.resourceSpans[0]; 79 | 80 | expect(resourceSpan.scopeSpans.length).toBe(1); 81 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('root-span'); 82 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(1); 83 | const span = resourceSpan.scopeSpans[0].spans[0]; 84 | 85 | // Sanity check trace ID 86 | expect(span.spanId.length).toBe(16); 87 | expect(span.spanId).toMatch(/[a-f0-9]{16}/); 88 | }); 89 | }); 90 | 91 | describe('Root span', () => { 92 | test('Default root span', async () => { 93 | devWorker = await startWorker('test/scripts/api/root-span.ts'); 94 | 95 | const res = await devWorker.fetch('http://worker/test'); 96 | 97 | expect(res.status).toBe(200); 98 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 99 | 100 | const traceId = res.headers.get('x-trace-id'); 101 | if (traceId === null) { 102 | expect(traceId).not.toBeNull(); 103 | return; 104 | } 105 | const trace = await getTrace(collectorWorker, traceId); 106 | 107 | expect(trace.resourceSpans.length).toBe(1); 108 | const resourceSpan = trace.resourceSpans[0]; 109 | const resource = resourceSpan.resource; 110 | 111 | // Validate default resource attributes 112 | expect( 113 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SERVICE_NAME), 114 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SERVICE_NAME, value: { stringValue: 'root-span' } }); 115 | expect( 116 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_NAME), 117 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_NAME, value: { stringValue: 'workers-tracing' } }); 118 | expect( 119 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_LANG), 120 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_LANG, value: { stringValue: 'javascript' } }); 121 | expect( 122 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_VERSION), 123 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_VERSION, value: { stringValue: '__VERSION__' } }); 124 | expect( 125 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.RUNTIME_NAME), 126 | ).toStrictEqual({ key: ATTRIBUTE_NAME.RUNTIME_NAME, value: { stringValue: 'Cloudflare-Workers' } }); 127 | 128 | // Check spans 129 | expect(resourceSpan.scopeSpans.length).toBe(1); 130 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('root-span'); 131 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(1); 132 | const span = resourceSpan.scopeSpans[0].spans[0]; 133 | 134 | // Validate root span 135 | expect(span.name).toBe('Request (fetch event)'); 136 | expect(span.endTimeUnixNano).not.toBe(0); 137 | }); 138 | 139 | test('Root span with resource attributes', async () => { 140 | devWorker = await startWorker('test/scripts/api/root-span-resource-attributes.ts'); 141 | 142 | const res = await devWorker.fetch('http://worker/test'); 143 | 144 | expect(res.status).toBe(200); 145 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 146 | 147 | const traceId = res.headers.get('x-trace-id'); 148 | if (traceId === null) { 149 | expect(traceId).not.toBeNull(); 150 | return; 151 | } 152 | const trace = await getTrace(collectorWorker, traceId); 153 | 154 | expect(trace.resourceSpans.length).toBe(1); 155 | const resourceSpan = trace.resourceSpans[0]; 156 | const resource = resourceSpan.resource; 157 | 158 | // Validate default resource attributes 159 | expect( 160 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SERVICE_NAME), 161 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SERVICE_NAME, value: { stringValue: 'root-span-resource-attributes' } }); 162 | expect( 163 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_NAME), 164 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_NAME, value: { stringValue: 'workers-tracing' } }); 165 | expect( 166 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_LANG), 167 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_LANG, value: { stringValue: 'javascript' } }); 168 | expect( 169 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_VERSION), 170 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_VERSION, value: { stringValue: '__VERSION__' } }); 171 | 172 | // Validate custom resource attributes 173 | expect( 174 | resource.attributes.find((attribute) => attribute.key === 'example'), 175 | ).toStrictEqual({ key: 'example', value: { boolValue: true } }); 176 | expect( 177 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.RUNTIME_NAME), 178 | ).toStrictEqual({ key: ATTRIBUTE_NAME.RUNTIME_NAME, value: { stringValue: 'blob-runtime' } }); 179 | 180 | // Check spans 181 | expect(resourceSpan.scopeSpans.length).toBe(1); 182 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('root-span-resource-attributes'); 183 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(1); 184 | const span = resourceSpan.scopeSpans[0].spans[0]; 185 | 186 | // Validate root span 187 | expect(span.name).toBe('Request (fetch event)'); 188 | expect(span.endTimeUnixNano).not.toBe(0); 189 | }); 190 | 191 | test('Root span with attributes', async () => { 192 | devWorker = await startWorker('test/scripts/api/root-span-attributes.ts'); 193 | 194 | const res = await devWorker.fetch('http://worker/test'); 195 | 196 | expect(res.status).toBe(200); 197 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 198 | 199 | const traceId = res.headers.get('x-trace-id'); 200 | if (traceId === null) { 201 | expect(traceId).not.toBeNull(); 202 | return; 203 | } 204 | const trace = await getTrace(collectorWorker, traceId); 205 | 206 | expect(trace.resourceSpans.length).toBe(1); 207 | const resourceSpan = trace.resourceSpans[0]; 208 | const resource = resourceSpan.resource; 209 | 210 | // Validate default resource attributes 211 | expect( 212 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SERVICE_NAME), 213 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SERVICE_NAME, value: { stringValue: 'root-span-attributes' } }); 214 | expect( 215 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_NAME), 216 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_NAME, value: { stringValue: 'workers-tracing' } }); 217 | expect( 218 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_LANG), 219 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_LANG, value: { stringValue: 'javascript' } }); 220 | expect( 221 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_VERSION), 222 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_VERSION, value: { stringValue: '__VERSION__' } }); 223 | expect( 224 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.RUNTIME_NAME), 225 | ).toStrictEqual({ key: ATTRIBUTE_NAME.RUNTIME_NAME, value: { stringValue: 'Cloudflare-Workers' } }); 226 | 227 | // Check spans 228 | expect(resourceSpan.scopeSpans.length).toBe(1); 229 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('root-span-attributes'); 230 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(1); 231 | const span = resourceSpan.scopeSpans[0].spans[0]; 232 | 233 | // Validate root span 234 | expect(span.name).toBe('Request (fetch event)'); 235 | expect(span.endTimeUnixNano).not.toBe(0); 236 | 237 | // Validate custom attributes 238 | expect( 239 | span.attributes.find((attribute) => attribute.key === 'customAttribute'), 240 | ).toStrictEqual({ key: 'customAttribute', value: { intValue: 1337 } }); 241 | expect( 242 | span.attributes.find((attribute) => attribute.key === 'workersTracing'), 243 | ).toStrictEqual({ key: 'workersTracing', value: { boolValue: true } }); 244 | }); 245 | 246 | test('Root span with status', async () => { 247 | devWorker = await startWorker('test/scripts/api/root-span-status.ts'); 248 | 249 | const res = await devWorker.fetch('http://worker/test'); 250 | 251 | expect(res.status).toBe(200); 252 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 253 | 254 | const traceId = res.headers.get('x-trace-id'); 255 | if (traceId === null) { 256 | expect(traceId).not.toBeNull(); 257 | return; 258 | } 259 | const trace = await getTrace(collectorWorker, traceId); 260 | 261 | expect(trace.resourceSpans.length).toBe(1); 262 | const resourceSpan = trace.resourceSpans[0]; 263 | 264 | // Check spans 265 | expect(resourceSpan.scopeSpans.length).toBe(1); 266 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('root-span-status'); 267 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(1); 268 | const span = resourceSpan.scopeSpans[0].spans[0]; 269 | 270 | // Validate root span 271 | expect(span.name).toBe('Request (fetch event)'); 272 | expect(span.endTimeUnixNano).not.toBe(0); 273 | 274 | // Validate status 275 | expect(span.status).toStrictEqual({ code: 1 }); 276 | }); 277 | 278 | test('Root span with events', async () => { 279 | devWorker = await startWorker('test/scripts/api/root-span-events.ts'); 280 | 281 | const res = await devWorker.fetch('http://worker/test'); 282 | 283 | expect(res.status).toBe(200); 284 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 285 | 286 | const traceId = res.headers.get('x-trace-id'); 287 | if (traceId === null) { 288 | expect(traceId).not.toBeNull(); 289 | return; 290 | } 291 | const trace = await getTrace(collectorWorker, traceId); 292 | 293 | expect(trace.resourceSpans.length).toBe(1); 294 | const resourceSpan = trace.resourceSpans[0]; 295 | 296 | // Check spans 297 | expect(resourceSpan.scopeSpans.length).toBe(1); 298 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('root-span-events'); 299 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(1); 300 | const span = resourceSpan.scopeSpans[0].spans[0]; 301 | 302 | // Validate root span 303 | expect(span.name).toBe('Request (fetch event)'); 304 | expect(span.endTimeUnixNano).not.toBe(0); 305 | 306 | // Validate events 307 | expect(span.events.length).toBe(1); 308 | expect(span.events[0].name).toBe('Fetch done'); 309 | expect(span.events[0].timeUnixNano).not.toBe(0); 310 | }); 311 | 312 | test('Root span with links', async () => { 313 | devWorker = await startWorker('test/scripts/api/root-span-links.ts'); 314 | 315 | const res = await devWorker.fetch('http://worker/test'); 316 | 317 | expect(res.status).toBe(200); 318 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 319 | 320 | const traceId = res.headers.get('x-trace-id'); 321 | if (traceId === null) { 322 | expect(traceId).not.toBeNull(); 323 | return; 324 | } 325 | const trace = await getTrace(collectorWorker, traceId); 326 | 327 | expect(trace.resourceSpans.length).toBe(1); 328 | const resourceSpan = trace.resourceSpans[0]; 329 | 330 | // Check spans 331 | expect(resourceSpan.scopeSpans.length).toBe(1); 332 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('root-span-links'); 333 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(1); 334 | const span = resourceSpan.scopeSpans[0].spans[0]; 335 | 336 | // Validate root span 337 | expect(span.name).toBe('Request (fetch event)'); 338 | expect(span.endTimeUnixNano).not.toBe(0); 339 | 340 | // Validate links 341 | expect(span.links.length).toBe(2); 342 | expect(span.links[0]).toStrictEqual({ 343 | traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', 344 | spanId: 'bbbbbbbbbbbbbbbb', 345 | attributes: [], 346 | }); 347 | expect(span.links[1]).toStrictEqual({ 348 | traceId: 'cccccccccccccccccccccccccccccccc', 349 | spanId: 'dddddddddddddddd', 350 | attributes: [ 351 | { 352 | key: 'link', 353 | value: { 354 | intValue: 2, 355 | }, 356 | }, 357 | { 358 | key: 'muchWow', 359 | value: { 360 | boolValue: true, 361 | }, 362 | }, 363 | ], 364 | }); 365 | }); 366 | }); 367 | }); 368 | 369 | describe('Default attributes', () => { 370 | test('Attributes are all set', async () => { 371 | devWorker = await startWorker('test/scripts/api/root-span.ts'); 372 | 373 | const res = await devWorker.fetch('http://worker/test'); 374 | 375 | expect(res.status).toBe(200); 376 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 377 | 378 | const traceId = res.headers.get('x-trace-id'); 379 | if (traceId === null) { 380 | expect(traceId).not.toBeNull(); 381 | return; 382 | } 383 | const trace = await getTrace(collectorWorker, traceId); 384 | 385 | expect(trace.resourceSpans.length).toBe(1); 386 | const resourceSpan = trace.resourceSpans[0]; 387 | const resource = resourceSpan.resource; 388 | 389 | // Validate default attributes 390 | expect( 391 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SERVICE_NAME), 392 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SERVICE_NAME, value: { stringValue: 'root-span' } }); 393 | expect( 394 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_NAME), 395 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_NAME, value: { stringValue: 'workers-tracing' } }); 396 | expect( 397 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_LANG), 398 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_LANG, value: { stringValue: 'javascript' } }); 399 | expect( 400 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_VERSION), 401 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_VERSION, value: { stringValue: '__VERSION__' } }); 402 | expect( 403 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.RUNTIME_NAME), 404 | ).toStrictEqual({ key: ATTRIBUTE_NAME.RUNTIME_NAME, value: { stringValue: 'Cloudflare-Workers' } }); 405 | }); 406 | 407 | test('Attributes work with no compat date', async () => { 408 | devWorker = await startWorker('test/scripts/api/root-span.ts', { 409 | // Set compat date to September 2021, this is the earliest compat date possible 410 | // and what it will default to if none is provided 411 | compatibilityDate: '2021-09-14', 412 | }); 413 | 414 | const res = await devWorker.fetch('http://worker/test'); 415 | 416 | expect(res.status).toBe(200); 417 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 418 | 419 | const traceId = res.headers.get('x-trace-id'); 420 | if (traceId === null) { 421 | expect(traceId).not.toBeNull(); 422 | return; 423 | } 424 | const trace = await getTrace(collectorWorker, traceId); 425 | 426 | expect(trace.resourceSpans.length).toBe(1); 427 | const resourceSpan = trace.resourceSpans[0]; 428 | const resource = resourceSpan.resource; 429 | 430 | // Validate default attributes 431 | expect( 432 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SERVICE_NAME), 433 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SERVICE_NAME, value: { stringValue: 'root-span' } }); 434 | expect( 435 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_NAME), 436 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_NAME, value: { stringValue: 'workers-tracing' } }); 437 | expect( 438 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_LANG), 439 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_LANG, value: { stringValue: 'javascript' } }); 440 | expect( 441 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_VERSION), 442 | ).toStrictEqual({ key: ATTRIBUTE_NAME.SDK_VERSION, value: { stringValue: '__VERSION__' } }); 443 | expect( 444 | resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.RUNTIME_NAME), 445 | ).toStrictEqual({ key: ATTRIBUTE_NAME.RUNTIME_NAME, value: { stringValue: 'Unknown' } }); 446 | }); 447 | }); 448 | 449 | describe('buildSpan', () => { 450 | test('Can build basic span', async () => { 451 | devWorker = await startWorker('test/scripts/api/span-builder/basic.ts'); 452 | const trace = await requestAndGetTrace(devWorker, collectorWorker, URL); 453 | 454 | expect(trace.resourceSpans.length).toBe(1); 455 | const resourceSpan = trace.resourceSpans[0]; 456 | 457 | // Check spans 458 | expect(resourceSpan.scopeSpans.length).toBe(1); 459 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('span-builder-basic'); 460 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(2); 461 | const span = resourceSpan.scopeSpans[0].spans[0]; 462 | 463 | // Validate root span 464 | expect(span.name).toBe('Request (fetch event)'); 465 | expect(span.endTimeUnixNano).not.toBe(0); 466 | 467 | // Validate builder span 468 | const builderSpan = resourceSpan.scopeSpans[0].spans[1]; 469 | expect(builderSpan.name).toBe('fetch'); 470 | expect(builderSpan.startTimeUnixNano).not.toBe(0); 471 | expect(builderSpan.endTimeUnixNano).not.toBe(0); 472 | expect(builderSpan.endTimeUnixNano - builderSpan.startTimeUnixNano).not.toBe(0); 473 | }); 474 | 475 | test('Can add attributes', async () => { 476 | devWorker = await startWorker('test/scripts/api/span-builder/attributes.ts'); 477 | const trace = await requestAndGetTrace(devWorker, collectorWorker, URL); 478 | 479 | expect(trace.resourceSpans.length).toBe(1); 480 | const resourceSpan = trace.resourceSpans[0]; 481 | 482 | // Check spans 483 | expect(resourceSpan.scopeSpans.length).toBe(1); 484 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('span-builder-attributes'); 485 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(2); 486 | const span = resourceSpan.scopeSpans[0].spans[0]; 487 | 488 | // Validate root span 489 | expect(span.name).toBe('Request (fetch event)'); 490 | expect(span.endTimeUnixNano).not.toBe(0); 491 | 492 | // Validate builder span 493 | const builderSpan = resourceSpan.scopeSpans[0].spans[1]; 494 | expect(builderSpan.name).toBe('fetch'); 495 | expect(builderSpan.startTimeUnixNano).not.toBe(0); 496 | expect(builderSpan.endTimeUnixNano).not.toBe(0); 497 | expect(builderSpan.endTimeUnixNano - builderSpan.startTimeUnixNano).not.toBe(0); 498 | 499 | // Should have 8 attributes 500 | expect(builderSpan.attributes.length).toBe(8); 501 | 502 | // Single value attributes 503 | expect(builderSpan.attributes.find((attribute) => attribute.key === 'str')) 504 | .toStrictEqual({ key: 'str', value: { stringValue: 'example.com' } }); 505 | expect(builderSpan.attributes.find((attribute) => attribute.key === 'int')) 506 | .toStrictEqual({ key: 'int', value: { intValue: 1337 } }); 507 | expect(builderSpan.attributes.find((attribute) => attribute.key === 'double')) 508 | .toStrictEqual({ key: 'double', value: { doubleValue: 13.37 } }); 509 | expect(builderSpan.attributes.find((attribute) => attribute.key === 'bool')) 510 | .toStrictEqual({ key: 'bool', value: { boolValue: true } }); 511 | 512 | // Array attributes 513 | expect(builderSpan.attributes.find((attribute) => attribute.key === 'strArray')) 514 | .toStrictEqual({ key: 'strArray', value: { 515 | arrayValue: { 516 | values: [ 517 | { stringValue: 'a' }, 518 | { stringValue: 'b' }, 519 | { stringValue: 'c' }, 520 | ], 521 | }, 522 | }}); 523 | expect(builderSpan.attributes.find((attribute) => attribute.key === 'intArray')) 524 | .toStrictEqual({ key: 'intArray', value: { 525 | arrayValue: { 526 | values: [ 527 | { intValue: 1 }, 528 | { intValue: 2 }, 529 | { intValue: 3 }, 530 | ], 531 | }, 532 | }}); 533 | expect(builderSpan.attributes.find((attribute) => attribute.key === 'doubleArray')) 534 | .toStrictEqual({ key: 'doubleArray', value: { 535 | arrayValue: { 536 | values: [ 537 | { doubleValue: 1.1 }, 538 | { doubleValue: 2.2 }, 539 | { doubleValue: 3.3 }, 540 | ], 541 | }, 542 | }}); 543 | expect(builderSpan.attributes.find((attribute) => attribute.key === 'boolArray')) 544 | .toStrictEqual({ key: 'boolArray', value: { 545 | arrayValue: { 546 | values: [ 547 | { boolValue: true }, 548 | { boolValue: false }, 549 | { boolValue: true }, 550 | ], 551 | }, 552 | }}); 553 | }); 554 | 555 | test('Can add attributes and remove', async () => { 556 | devWorker = await startWorker('test/scripts/api/span-builder/add-remove-attributes.ts'); 557 | const trace = await requestAndGetTrace(devWorker, collectorWorker, URL); 558 | 559 | expect(trace.resourceSpans.length).toBe(1); 560 | const resourceSpan = trace.resourceSpans[0]; 561 | 562 | // Check spans 563 | expect(resourceSpan.scopeSpans.length).toBe(1); 564 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('span-builder-add-remove-attributes'); 565 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(2); 566 | const span = resourceSpan.scopeSpans[0].spans[0]; 567 | 568 | // Validate root span 569 | expect(span.name).toBe('Request (fetch event)'); 570 | expect(span.endTimeUnixNano).not.toBe(0); 571 | 572 | // Validate builder span 573 | const builderSpan = resourceSpan.scopeSpans[0].spans[1]; 574 | expect(builderSpan.name).toBe('fetch'); 575 | expect(builderSpan.startTimeUnixNano).not.toBe(0); 576 | expect(builderSpan.endTimeUnixNano).not.toBe(0); 577 | expect(builderSpan.endTimeUnixNano - builderSpan.startTimeUnixNano).not.toBe(0); 578 | 579 | // "str" was removed, we should only have 1 left 580 | expect(builderSpan.attributes.length).toBe(1); 581 | expect(builderSpan.attributes.find((attribute) => attribute.key === 'int')) 582 | .toStrictEqual({ key: 'int', value: { intValue: 1337 } }); 583 | }); 584 | 585 | test('Can set status', async () => { 586 | devWorker = await startWorker('test/scripts/api/span-builder/status.ts'); 587 | const trace = await requestAndGetTrace(devWorker, collectorWorker, URL); 588 | 589 | expect(trace.resourceSpans.length).toBe(1); 590 | const resourceSpan = trace.resourceSpans[0]; 591 | 592 | // Check spans 593 | expect(resourceSpan.scopeSpans.length).toBe(1); 594 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('span-builder-status'); 595 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(2); 596 | const span = resourceSpan.scopeSpans[0].spans[0]; 597 | 598 | // Validate root span 599 | expect(span.name).toBe('Request (fetch event)'); 600 | expect(span.endTimeUnixNano).not.toBe(0); 601 | 602 | // Validate builder span 603 | const builderSpan = resourceSpan.scopeSpans[0].spans[1]; 604 | expect(builderSpan.name).toBe('fetch'); 605 | expect(builderSpan.startTimeUnixNano).not.toBe(0); 606 | expect(builderSpan.endTimeUnixNano).not.toBe(0); 607 | expect(builderSpan.endTimeUnixNano - builderSpan.startTimeUnixNano).not.toBe(0); 608 | 609 | // Validate status 610 | expect(builderSpan.status).toStrictEqual({ code: StatusCode.OK }); 611 | }); 612 | 613 | test('Can add event', async () => { 614 | devWorker = await startWorker('test/scripts/api/span-builder/event.ts'); 615 | const trace = await requestAndGetTrace(devWorker, collectorWorker, URL); 616 | 617 | expect(trace.resourceSpans.length).toBe(1); 618 | const resourceSpan = trace.resourceSpans[0]; 619 | 620 | // Check spans 621 | expect(resourceSpan.scopeSpans.length).toBe(1); 622 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('span-builder-event'); 623 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(2); 624 | const span = resourceSpan.scopeSpans[0].spans[0]; 625 | 626 | // Validate root span 627 | expect(span.name).toBe('Request (fetch event)'); 628 | expect(span.endTimeUnixNano).not.toBe(0); 629 | 630 | // Validate builder span 631 | const builderSpan = resourceSpan.scopeSpans[0].spans[1]; 632 | expect(builderSpan.name).toBe('fetch'); 633 | expect(builderSpan.startTimeUnixNano).not.toBe(0); 634 | expect(builderSpan.endTimeUnixNano).not.toBe(0); 635 | expect(builderSpan.endTimeUnixNano - builderSpan.startTimeUnixNano).not.toBe(0); 636 | 637 | // Validate events 638 | expect(builderSpan.events.length).toBe(2); 639 | 640 | expect(builderSpan.events[0].name).toBe('Span started'); 641 | expect(builderSpan.events[0].attributes.length).toBe(0); 642 | expect(builderSpan.events[0].timeUnixNano).not.toBe(0); 643 | 644 | expect(builderSpan.events[1].name).toBe('Fetch done'); 645 | expect(builderSpan.events[1].attributes.length).toBe(1); 646 | expect(builderSpan.events[1].attributes[0]) 647 | .toStrictEqual({ key: 'host', value: { stringValue: 'example.com' } }); 648 | expect(builderSpan.events[1].timeUnixNano).not.toBe(0); 649 | }); 650 | 651 | test('Can add links', async () => { 652 | devWorker = await startWorker('test/scripts/api/span-builder/links.ts'); 653 | const trace = await requestAndGetTrace(devWorker, collectorWorker, URL); 654 | 655 | expect(trace.resourceSpans.length).toBe(1); 656 | const resourceSpan = trace.resourceSpans[0]; 657 | 658 | // Check spans 659 | expect(resourceSpan.scopeSpans.length).toBe(1); 660 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('span-builder-links'); 661 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(2); 662 | const span = resourceSpan.scopeSpans[0].spans[0]; 663 | 664 | // Validate root span 665 | expect(span.name).toBe('Request (fetch event)'); 666 | expect(span.endTimeUnixNano).not.toBe(0); 667 | 668 | // Validate builder span 669 | const builderSpan = resourceSpan.scopeSpans[0].spans[1]; 670 | expect(builderSpan.name).toBe('fetch'); 671 | expect(builderSpan.startTimeUnixNano).not.toBe(0); 672 | expect(builderSpan.endTimeUnixNano).not.toBe(0); 673 | expect(builderSpan.endTimeUnixNano - builderSpan.startTimeUnixNano).not.toBe(0); 674 | 675 | // Validate links 676 | expect(builderSpan.links.length).toBe(2); 677 | 678 | expect(builderSpan.links[0].traceId).toBe(span.traceId); 679 | expect(builderSpan.links[0].spanId).toBe(span.spanId); 680 | expect(builderSpan.links[0].attributes.length).toBe(0); 681 | 682 | expect(builderSpan.links[1].traceId).toBe('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'); 683 | expect(builderSpan.links[1].spanId).toBe('aaaaaaaaaaaaaaaa'); 684 | expect(builderSpan.links[1].attributes.length).toBe(1); 685 | expect(builderSpan.links[1].attributes[0]) 686 | .toStrictEqual({ key: 'service', value: { stringValue: 'example' } }); 687 | }); 688 | }); 689 | }); 690 | -------------------------------------------------------------------------------- /test/otlp-exporter.test.ts: -------------------------------------------------------------------------------- 1 | import { OtlpJson } from 'src/transformers/otlp'; 2 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 3 | import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; 4 | import { UnstableDevWorker } from 'wrangler'; 5 | import { getTrace } from './utils/trace'; 6 | import { startCollector, startWorker } from './utils/worker'; 7 | 8 | let devWorker: UnstableDevWorker; 9 | let collectorWorker: UnstableDevWorker; 10 | 11 | describe('Test OTLP Exporter', () => { 12 | beforeAll(async () => { 13 | collectorWorker = await startCollector({ port: 4318 }); 14 | }); 15 | 16 | afterEach(async () => { 17 | if (devWorker) { 18 | await devWorker.stop(); 19 | await devWorker.waitUntilExit(); 20 | } 21 | }); 22 | 23 | afterAll(async () => { 24 | if (collectorWorker) { 25 | await collectorWorker.stop(); 26 | await collectorWorker.waitUntilExit(); 27 | } 28 | }); 29 | 30 | test('Basic trace should transform correctly', async () => { 31 | devWorker = await startWorker('test/scripts/otlp/basic.ts'); 32 | 33 | const res = await devWorker.fetch('http://worker/test'); 34 | 35 | expect(res.status).toBe(200); 36 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 37 | 38 | const traceId = res.headers.get('x-trace-id'); 39 | if (traceId === null) { 40 | expect(traceId).not.toBeNull(); 41 | return; 42 | } 43 | const trace = await getTrace(collectorWorker, traceId); 44 | 45 | expect(trace.resourceSpans.length).toBe(1); 46 | const resourceSpan = trace.resourceSpans[0]; 47 | 48 | expect(resourceSpan.scopeSpans.length).toBe(1); 49 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('otlp-basic'); 50 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(1); 51 | 52 | const span = resourceSpan.scopeSpans[0].spans[0]; 53 | expect(span.traceId).toBe(traceId); 54 | expect(span.name).toBe('Request (fetch event)'); 55 | }); 56 | 57 | describe('Resource', () => { 58 | test('Default attributes are put on resource', async () => { 59 | devWorker = await startWorker('test/scripts/otlp/basic.ts'); 60 | 61 | const res = await devWorker.fetch('http://worker/test'); 62 | 63 | expect(res.status).toBe(200); 64 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 65 | 66 | const traceId = res.headers.get('x-trace-id'); 67 | if (traceId === null) { 68 | expect(traceId).not.toBeNull(); 69 | return; 70 | } 71 | const trace = await getTrace(collectorWorker, traceId); 72 | 73 | expect(trace.resourceSpans.length).toBe(1); 74 | const resourceSpan = trace.resourceSpans[0]; 75 | 76 | expect(resourceSpan.scopeSpans.length).toBe(1); 77 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('otlp-basic'); 78 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(1); 79 | 80 | const resource = resourceSpan.resource; 81 | 82 | // Check attributes 83 | expect(resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SERVICE_NAME)) 84 | .toStrictEqual({ key: ATTRIBUTE_NAME.SERVICE_NAME, value: { stringValue: 'otlp-basic' } }); 85 | expect(resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_NAME)) 86 | .toStrictEqual({ key: ATTRIBUTE_NAME.SDK_NAME, value: { stringValue: 'workers-tracing' } }); 87 | expect(resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_LANG)) 88 | .toStrictEqual({ key: ATTRIBUTE_NAME.SDK_LANG, value: { stringValue: 'javascript' } }); 89 | expect(resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_VERSION)) 90 | .toStrictEqual({ key: ATTRIBUTE_NAME.SDK_VERSION, value: { stringValue: '__VERSION__' } }); 91 | expect(resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.RUNTIME_NAME)) 92 | .toStrictEqual({ key: ATTRIBUTE_NAME.RUNTIME_NAME, value: { stringValue: 'Cloudflare-Workers' } }); 93 | }); 94 | 95 | test('You can add attributes on resource', async () => { 96 | devWorker = await startWorker('test/scripts/otlp/resource-attributes.ts'); 97 | 98 | const res = await devWorker.fetch('http://worker/test'); 99 | 100 | expect(res.status).toBe(200); 101 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 102 | 103 | const traceId = res.headers.get('x-trace-id'); 104 | if (traceId === null) { 105 | expect(traceId).not.toBeNull(); 106 | return; 107 | } 108 | const trace = await getTrace(collectorWorker, traceId); 109 | 110 | expect(trace.resourceSpans.length).toBe(1); 111 | const resourceSpan = trace.resourceSpans[0]; 112 | 113 | expect(resourceSpan.scopeSpans.length).toBe(1); 114 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('resource-attributes'); 115 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(1); 116 | 117 | const resource = resourceSpan.resource; 118 | 119 | // Check attributes 120 | expect(resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SERVICE_NAME)) 121 | .toStrictEqual({ key: ATTRIBUTE_NAME.SERVICE_NAME, value: { stringValue: 'resource-attributes' } }); 122 | expect(resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_NAME)) 123 | .toStrictEqual({ key: ATTRIBUTE_NAME.SDK_NAME, value: { stringValue: 'workers-tracing' } }); 124 | expect(resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_LANG)) 125 | .toStrictEqual({ key: ATTRIBUTE_NAME.SDK_LANG, value: { stringValue: 'javascript' } }); 126 | expect(resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.SDK_VERSION)) 127 | .toStrictEqual({ key: ATTRIBUTE_NAME.SDK_VERSION, value: { stringValue: '__VERSION__' } }); 128 | 129 | // Custom attributes 130 | expect(resource.attributes.find((attribute) => attribute.key === 'exampleAttribute')) 131 | .toStrictEqual({ key: 'exampleAttribute', value: { boolValue: true } }); 132 | expect(resource.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.RUNTIME_NAME)) 133 | .toStrictEqual({ key: ATTRIBUTE_NAME.RUNTIME_NAME, value: { stringValue: 'blob-runtime' } }); 134 | }); 135 | }); 136 | 137 | describe('Single span', () => { 138 | test('You can add a single span', async () => { 139 | devWorker = await startWorker('test/scripts/otlp/single-span.ts'); 140 | 141 | const res = await devWorker.fetch('http://worker/test'); 142 | 143 | expect(res.status).toBe(200); 144 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 145 | 146 | const traceId = res.headers.get('x-trace-id'); 147 | if (traceId === null) { 148 | expect(traceId).not.toBeNull(); 149 | return; 150 | } 151 | const trace = await getTrace(collectorWorker, traceId); 152 | 153 | // Root + child 154 | expect(trace.resourceSpans.length).toBe(1); 155 | const resourceSpan = trace.resourceSpans[0]; 156 | 157 | expect(resourceSpan.scopeSpans.length).toBe(1); 158 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('single-span'); 159 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(2); 160 | 161 | const spans = resourceSpan.scopeSpans[0].spans; 162 | 163 | // Root span 164 | const rootSpan = spans[0]; 165 | expect(rootSpan.traceId).toBe(traceId); 166 | expect(rootSpan.name).toBe('Request (fetch event)'); 167 | expect(rootSpan.endTimeUnixNano).not.toBe(0); 168 | 169 | // Child span 170 | const childSpan = spans[1]; 171 | expect(childSpan.traceId).toBe(traceId); 172 | expect(childSpan.parentSpanId).toBe(rootSpan.spanId); 173 | expect(childSpan.name).toBe(SPAN_NAME.FETCH); 174 | expect(childSpan.endTimeUnixNano).not.toBe(0); 175 | }); 176 | 177 | test('You can add a single span with attributes', async () => { 178 | devWorker = await startWorker('test/scripts/otlp/single-span-attributes.ts'); 179 | 180 | const res = await devWorker.fetch('http://worker/test'); 181 | 182 | expect(res.status).toBe(200); 183 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 184 | 185 | const traceId = res.headers.get('x-trace-id'); 186 | if (traceId === null) { 187 | expect(traceId).not.toBeNull(); 188 | return; 189 | } 190 | const trace = await getTrace(collectorWorker, traceId); 191 | 192 | // Root + child 193 | expect(trace.resourceSpans.length).toBe(1); 194 | const resourceSpan = trace.resourceSpans[0]; 195 | 196 | expect(resourceSpan.scopeSpans.length).toBe(1); 197 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('single-span-attributes'); 198 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(2); 199 | 200 | const spans = resourceSpan.scopeSpans[0].spans; 201 | 202 | // Root span 203 | const rootSpan = spans[0]; 204 | expect(rootSpan.traceId).toBe(traceId); 205 | expect(rootSpan.name).toBe('Request (fetch event)'); 206 | expect(rootSpan.endTimeUnixNano).not.toBe(0); 207 | 208 | // Child span 209 | const childSpan = spans[1]; 210 | expect(childSpan.traceId).toBe(traceId); 211 | expect(childSpan.parentSpanId).toBe(rootSpan.spanId); 212 | expect(childSpan.name).toBe(SPAN_NAME.FETCH); 213 | expect(childSpan.endTimeUnixNano).not.toBe(0); 214 | expect(childSpan.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.HTTP_HOST)) 215 | .toStrictEqual({ key: ATTRIBUTE_NAME.HTTP_HOST, value: { stringValue: 'example.com' } }); 216 | }); 217 | 218 | test('You can add a single span with events', async () => { 219 | devWorker = await startWorker('test/scripts/otlp/single-span-events.ts'); 220 | 221 | const res = await devWorker.fetch('http://worker/test'); 222 | 223 | expect(res.status).toBe(200); 224 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 225 | 226 | const traceId = res.headers.get('x-trace-id'); 227 | if (traceId === null) { 228 | expect(traceId).not.toBeNull(); 229 | return; 230 | } 231 | const trace = await getTrace(collectorWorker, traceId); 232 | 233 | // Root + child 234 | expect(trace.resourceSpans.length).toBe(1); 235 | const resourceSpan = trace.resourceSpans[0]; 236 | 237 | expect(resourceSpan.scopeSpans.length).toBe(1); 238 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('single-span-events'); 239 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(2); 240 | 241 | const spans = resourceSpan.scopeSpans[0].spans; 242 | 243 | // Root span 244 | const rootSpan = spans[0]; 245 | expect(rootSpan.traceId).toBe(traceId); 246 | expect(rootSpan.name).toBe('Request (fetch event)'); 247 | expect(rootSpan.endTimeUnixNano).not.toBe(0); 248 | 249 | // Child span 250 | const childSpan = spans[1]; 251 | expect(childSpan.traceId).toBe(traceId); 252 | expect(childSpan.parentSpanId).toBe(rootSpan.spanId); 253 | expect(childSpan.name).toBe(SPAN_NAME.FETCH); 254 | expect(childSpan.endTimeUnixNano).not.toBe(0); 255 | 256 | expect(childSpan.events.length).toBe(2); 257 | 258 | expect(childSpan.events[0].name).toBe('Fetch done'); 259 | expect(childSpan.events[0].timeUnixNano).not.toBe(0); 260 | expect(childSpan.events[0].attributes.length).toBe(1); 261 | expect(childSpan.events[0].attributes[0]) 262 | .toStrictEqual({ key: ATTRIBUTE_NAME.HTTP_HOST, value: { stringValue: 'example.com' } }); 263 | 264 | expect(childSpan.events[1].name).toBe('Response body parsed'); 265 | expect(childSpan.events[1].timeUnixNano).not.toBe(0); 266 | expect(childSpan.events[1].attributes.length).toBe(1); 267 | expect(childSpan.events[1].attributes[0]) 268 | .toStrictEqual({ key: 'parsed', value: { stringValue: 'text' } }); 269 | }); 270 | 271 | test('You can add a single span with attributes and events', async () => { 272 | devWorker = await startWorker('test/scripts/otlp/single-span-attributes-and-events.ts'); 273 | 274 | const res = await devWorker.fetch('http://worker/test'); 275 | 276 | expect(res.status).toBe(200); 277 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 278 | 279 | const traceId = res.headers.get('x-trace-id'); 280 | if (traceId === null) { 281 | expect(traceId).not.toBeNull(); 282 | return; 283 | } 284 | const trace = await getTrace(collectorWorker, traceId); 285 | 286 | // Root + child 287 | expect(trace.resourceSpans.length).toBe(1); 288 | const resourceSpan = trace.resourceSpans[0]; 289 | 290 | expect(resourceSpan.scopeSpans.length).toBe(1); 291 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('single-span-attributes-and-events'); 292 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(2); 293 | 294 | const spans = resourceSpan.scopeSpans[0].spans; 295 | 296 | // Root span 297 | const rootSpan = spans[0]; 298 | expect(rootSpan.traceId).toBe(traceId); 299 | expect(rootSpan.name).toBe('Request (fetch event)'); 300 | expect(rootSpan.endTimeUnixNano).not.toBe(0); 301 | 302 | // Child span 303 | const childSpan = spans[1]; 304 | expect(childSpan.traceId).toBe(traceId); 305 | expect(childSpan.parentSpanId).toBe(rootSpan.spanId); 306 | expect(childSpan.name).toBe(SPAN_NAME.FETCH); 307 | expect(childSpan.endTimeUnixNano).not.toBe(0); 308 | expect(childSpan.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.HTTP_HOST)) 309 | .toStrictEqual({ key: ATTRIBUTE_NAME.HTTP_HOST, value: { stringValue: 'example.com' } }); 310 | 311 | expect(childSpan.events.length).toBe(2); 312 | expect(childSpan.events[0].name).toBe('Fetch done'); 313 | expect(childSpan.events[0].timeUnixNano).not.toBe(0); 314 | expect(childSpan.events[0].attributes.length).toBe(1); 315 | expect(childSpan.events[0].attributes[0]) 316 | .toStrictEqual({ key: ATTRIBUTE_NAME.HTTP_HOST, value: { stringValue: 'example.com' } }); 317 | 318 | expect(childSpan.events[1].name).toBe('Response body parsed'); 319 | expect(childSpan.events[1].timeUnixNano).not.toBe(0); 320 | expect(childSpan.events[1].attributes.length).toBe(1); 321 | expect(childSpan.events[1].attributes[0]) 322 | .toStrictEqual({ key: 'parsed', value: { stringValue: 'text' } }); 323 | }); 324 | }); 325 | 326 | describe('Multiple spans', () => { 327 | test('You can add multiple spans', async () => { 328 | devWorker = await startWorker('test/scripts/otlp/multiple-spans.ts', { 329 | kv: [ { binding: 'KV', id: '' } ], 330 | }); 331 | 332 | const res = await devWorker.fetch('http://worker/test'); 333 | 334 | expect(res.status).toBe(200); 335 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 336 | 337 | const traceId = res.headers.get('x-trace-id'); 338 | if (traceId === null) { 339 | expect(traceId).not.toBeNull(); 340 | return; 341 | } 342 | const trace = await getTrace(collectorWorker, traceId); 343 | 344 | // Root + 2 children 345 | expect(trace.resourceSpans.length).toBe(1); 346 | const resourceSpan = trace.resourceSpans[0]; 347 | 348 | expect(resourceSpan.scopeSpans.length).toBe(1); 349 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('multiple-spans'); 350 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(3); 351 | 352 | const spans = resourceSpan.scopeSpans[0].spans; 353 | 354 | // Root span 355 | const rootSpan = spans[0]; 356 | expect(rootSpan.traceId).toBe(traceId); 357 | expect(rootSpan.name).toBe('Request (fetch event)'); 358 | expect(rootSpan.endTimeUnixNano).not.toBe(0); 359 | 360 | // First child span 361 | const firstChildSpan = spans[1]; 362 | expect(firstChildSpan.traceId).toBe(traceId); 363 | expect(firstChildSpan.parentSpanId).toBe(rootSpan.spanId); 364 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 365 | expect(firstChildSpan.endTimeUnixNano).not.toBe(0); 366 | 367 | // Second child span 368 | const secondChildSpan = spans[2]; 369 | expect(secondChildSpan.traceId).toBe(traceId); 370 | expect(secondChildSpan.parentSpanId).toBe(rootSpan.spanId); 371 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 372 | expect(secondChildSpan.endTimeUnixNano).not.toBe(0); 373 | }); 374 | 375 | test('You can add multiple spans with attributes', async () => { 376 | devWorker = await startWorker('test/scripts/otlp/multiple-spans-attributes.ts', { 377 | kv: [ { binding: 'KV', id: '' } ], 378 | }); 379 | 380 | const res = await devWorker.fetch('http://worker/test'); 381 | 382 | expect(res.status).toBe(200); 383 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 384 | 385 | const traceId = res.headers.get('x-trace-id'); 386 | if (traceId === null) { 387 | expect(traceId).not.toBeNull(); 388 | return; 389 | } 390 | const trace = await getTrace(collectorWorker, traceId); 391 | 392 | // Root + 2 children 393 | expect(trace.resourceSpans.length).toBe(1); 394 | const resourceSpan = trace.resourceSpans[0]; 395 | 396 | expect(resourceSpan.scopeSpans.length).toBe(1); 397 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('multiple-spans-attributes'); 398 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(3); 399 | 400 | const spans = resourceSpan.scopeSpans[0].spans; 401 | 402 | // Root span 403 | const rootSpan = spans[0]; 404 | expect(rootSpan.traceId).toBe(traceId); 405 | expect(rootSpan.name).toBe('Request (fetch event)'); 406 | expect(rootSpan.endTimeUnixNano).not.toBe(0); 407 | 408 | // First child span 409 | const firstChildSpan = spans[1]; 410 | expect(firstChildSpan.traceId).toBe(traceId); 411 | expect(firstChildSpan.parentSpanId).toBe(rootSpan.spanId); 412 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 413 | expect(firstChildSpan.endTimeUnixNano).not.toBe(0); 414 | expect(firstChildSpan.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.HTTP_HOST)) 415 | .toStrictEqual({ key: ATTRIBUTE_NAME.HTTP_HOST, value: { stringValue: 'example.com' } }); 416 | 417 | // Second child span 418 | const secondChildSpan = spans[2]; 419 | expect(secondChildSpan.traceId).toBe(traceId); 420 | expect(secondChildSpan.parentSpanId).toBe(rootSpan.spanId); 421 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 422 | expect(secondChildSpan.endTimeUnixNano).not.toBe(0); 423 | expect(secondChildSpan.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.KV_KEY)) 424 | .toStrictEqual({ key: ATTRIBUTE_NAME.KV_KEY, value: { stringValue: 'abc' } }); 425 | }); 426 | 427 | test('You can add multiple spans with events', async () => { 428 | devWorker = await startWorker('test/scripts/otlp/multiple-spans-events.ts', { 429 | kv: [ { binding: 'KV', id: '' } ], 430 | }); 431 | 432 | const res = await devWorker.fetch('http://worker/test'); 433 | 434 | expect(res.status).toBe(200); 435 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 436 | 437 | const traceId = res.headers.get('x-trace-id'); 438 | if (traceId === null) { 439 | expect(traceId).not.toBeNull(); 440 | return; 441 | } 442 | const trace = await getTrace(collectorWorker, traceId); 443 | 444 | // Root + 2 children 445 | expect(trace.resourceSpans.length).toBe(1); 446 | const resourceSpan = trace.resourceSpans[0]; 447 | 448 | expect(resourceSpan.scopeSpans.length).toBe(1); 449 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('multiple-spans-events'); 450 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(3); 451 | 452 | const spans = resourceSpan.scopeSpans[0].spans; 453 | 454 | // Root span 455 | const rootSpan = spans[0]; 456 | expect(rootSpan.traceId).toBe(traceId); 457 | expect(rootSpan.name).toBe('Request (fetch event)'); 458 | expect(rootSpan.endTimeUnixNano).not.toBe(0); 459 | 460 | // First child span 461 | const firstChildSpan = spans[1]; 462 | expect(firstChildSpan.traceId).toBe(traceId); 463 | expect(firstChildSpan.parentSpanId).toBe(rootSpan.spanId); 464 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 465 | expect(firstChildSpan.endTimeUnixNano).not.toBe(0); 466 | 467 | // - Events 468 | expect(firstChildSpan.events.length).toBe(2); 469 | expect(firstChildSpan.events[0].name).toBe('Fetch done'); 470 | expect(firstChildSpan.events[0].timeUnixNano).not.toBe(0); 471 | expect(firstChildSpan.events[0].attributes.length).toBe(1); 472 | expect(firstChildSpan.events[0].attributes[0]) 473 | .toStrictEqual({ key: ATTRIBUTE_NAME.HTTP_HOST, value: { stringValue: 'example.com' } }); 474 | 475 | expect(firstChildSpan.events[1].name).toBe('Response body parsed'); 476 | expect(firstChildSpan.events[1].timeUnixNano).not.toBe(0); 477 | expect(firstChildSpan.events[1].attributes.length).toBe(1); 478 | expect(firstChildSpan.events[1].attributes[0]) 479 | .toStrictEqual({ key: 'parsed', value: { stringValue: 'text' } }); 480 | 481 | // Second child span 482 | const secondChildSpan = spans[2]; 483 | expect(secondChildSpan.traceId).toBe(traceId); 484 | expect(secondChildSpan.parentSpanId).toBe(rootSpan.spanId); 485 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 486 | expect(secondChildSpan.endTimeUnixNano).not.toBe(0); 487 | 488 | // - Events 489 | expect(secondChildSpan.events.length).toBe(1); 490 | expect(secondChildSpan.events[0].name).toBe('KV get done'); 491 | expect(secondChildSpan.events[0].timeUnixNano).not.toBe(0); 492 | expect(secondChildSpan.events[0].attributes.length).toBe(1); 493 | expect(secondChildSpan.events[0].attributes[0]) 494 | .toStrictEqual({ key: ATTRIBUTE_NAME.KV_KEY, value: { stringValue: 'abc' } }); 495 | }); 496 | 497 | test('You can add multiple spans with attributes and events', async () => { 498 | devWorker = await startWorker('test/scripts/otlp/multiple-spans-attributes-and-events.ts', { 499 | kv: [ { binding: 'KV', id: '' } ], 500 | }); 501 | 502 | const res = await devWorker.fetch('http://worker/test'); 503 | 504 | expect(res.status).toBe(200); 505 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 506 | 507 | const traceId = res.headers.get('x-trace-id'); 508 | if (traceId === null) { 509 | expect(traceId).not.toBeNull(); 510 | return; 511 | } 512 | const trace = await getTrace(collectorWorker, traceId); 513 | 514 | // Root + 2 children 515 | expect(trace.resourceSpans.length).toBe(1); 516 | const resourceSpan = trace.resourceSpans[0]; 517 | 518 | expect(resourceSpan.scopeSpans.length).toBe(1); 519 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('multiple-spans-attributes-and-events'); 520 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(3); 521 | 522 | const spans = resourceSpan.scopeSpans[0].spans; 523 | 524 | // Root span 525 | const rootSpan = spans[0]; 526 | expect(rootSpan.traceId).toBe(traceId); 527 | expect(rootSpan.name).toBe('Request (fetch event)'); 528 | expect(rootSpan.endTimeUnixNano).not.toBe(0); 529 | 530 | // First child span 531 | const firstChildSpan = spans[1]; 532 | expect(firstChildSpan.traceId).toBe(traceId); 533 | expect(firstChildSpan.parentSpanId).toBe(rootSpan.spanId); 534 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 535 | expect(firstChildSpan.endTimeUnixNano).not.toBe(0); 536 | expect(firstChildSpan.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.HTTP_HOST)) 537 | .toStrictEqual({ key: ATTRIBUTE_NAME.HTTP_HOST, value: { stringValue: 'example.com' } }); 538 | 539 | // - Events 540 | expect(firstChildSpan.events.length).toBe(2); 541 | expect(firstChildSpan.events[0].name).toBe('Fetch done'); 542 | expect(firstChildSpan.events[0].timeUnixNano).not.toBe(0); 543 | expect(firstChildSpan.events[0].attributes.length).toBe(1); 544 | expect(firstChildSpan.events[0].attributes[0]) 545 | .toStrictEqual({ key: ATTRIBUTE_NAME.HTTP_HOST, value: { stringValue: 'example.com' } }); 546 | 547 | expect(firstChildSpan.events[1].name).toBe('Response body parsed'); 548 | expect(firstChildSpan.events[1].timeUnixNano).not.toBe(0); 549 | expect(firstChildSpan.events[1].attributes.length).toBe(1); 550 | expect(firstChildSpan.events[1].attributes[0]) 551 | .toStrictEqual({ key: 'parsed', value: { stringValue: 'text' } }); 552 | 553 | // Second child span 554 | const secondChildSpan = spans[2]; 555 | expect(secondChildSpan.traceId).toBe(traceId); 556 | expect(secondChildSpan.parentSpanId).toBe(rootSpan.spanId); 557 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 558 | expect(secondChildSpan.endTimeUnixNano).not.toBe(0); 559 | expect(secondChildSpan.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.KV_KEY)) 560 | .toStrictEqual({ key: ATTRIBUTE_NAME.KV_KEY, value: { stringValue: 'abc' } }); 561 | 562 | // - Events 563 | expect(secondChildSpan.events.length).toBe(1); 564 | expect(secondChildSpan.events[0].name).toBe('KV get done'); 565 | expect(secondChildSpan.events[0].timeUnixNano).not.toBe(0); 566 | expect(secondChildSpan.events[0].attributes.length).toBe(1); 567 | expect(secondChildSpan.events[0].attributes[0]) 568 | .toStrictEqual({ key: ATTRIBUTE_NAME.KV_KEY, value: { stringValue: 'abc' } }); 569 | }); 570 | }); 571 | 572 | describe('Child of child span', () => { 573 | test('You can add a child to a child span', async () => { 574 | devWorker = await startWorker('test/scripts/otlp/span-span.ts', { 575 | kv: [ { binding: 'KV', id: '' } ], 576 | }); 577 | 578 | const res = await devWorker.fetch('http://worker/test'); 579 | 580 | expect(res.status).toBe(200); 581 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 582 | 583 | const traceId = res.headers.get('x-trace-id'); 584 | if (traceId === null) { 585 | expect(traceId).not.toBeNull(); 586 | return; 587 | } 588 | const trace = await getTrace(collectorWorker, traceId); 589 | 590 | // Root + 2 children 591 | expect(trace.resourceSpans.length).toBe(1); 592 | const resourceSpan = trace.resourceSpans[0]; 593 | 594 | expect(resourceSpan.scopeSpans.length).toBe(1); 595 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('span-span'); 596 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(3); 597 | 598 | const spans = resourceSpan.scopeSpans[0].spans; 599 | 600 | // Root span 601 | const rootSpan = spans[0]; 602 | expect(rootSpan.traceId).toBe(traceId); 603 | expect(rootSpan.name).toBe('Request (fetch event)'); 604 | expect(rootSpan.endTimeUnixNano).not.toBe(0); 605 | 606 | // First child span 607 | const firstChildSpan = spans[1]; 608 | expect(firstChildSpan.traceId).toBe(traceId); 609 | expect(firstChildSpan.parentSpanId).toBe(rootSpan.spanId); 610 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 611 | expect(firstChildSpan.endTimeUnixNano).not.toBe(0); 612 | 613 | // Second child span 614 | const secondChildSpan = spans[2]; 615 | expect(secondChildSpan.traceId).toBe(traceId); 616 | // Validate this is a child of the first child 617 | expect(secondChildSpan.parentSpanId).toBe(firstChildSpan.spanId); 618 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 619 | expect(secondChildSpan.endTimeUnixNano).not.toBe(0); 620 | }); 621 | 622 | test('You can add a child to a child span with attributes', async () => { 623 | devWorker = await startWorker('test/scripts/otlp/span-span-attributes.ts', { 624 | kv: [ { binding: 'KV', id: '' } ], 625 | }); 626 | 627 | const res = await devWorker.fetch('http://worker/test'); 628 | 629 | expect(res.status).toBe(200); 630 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 631 | 632 | const traceId = res.headers.get('x-trace-id'); 633 | if (traceId === null) { 634 | expect(traceId).not.toBeNull(); 635 | return; 636 | } 637 | const trace = await getTrace(collectorWorker, traceId); 638 | 639 | // Root + 2 children 640 | expect(trace.resourceSpans.length).toBe(1); 641 | const resourceSpan = trace.resourceSpans[0]; 642 | 643 | expect(resourceSpan.scopeSpans.length).toBe(1); 644 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('span-span-attributes'); 645 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(3); 646 | 647 | const spans = resourceSpan.scopeSpans[0].spans; 648 | 649 | // Root span 650 | const rootSpan = spans[0]; 651 | expect(rootSpan.traceId).toBe(traceId); 652 | expect(rootSpan.name).toBe('Request (fetch event)'); 653 | expect(rootSpan.endTimeUnixNano).not.toBe(0); 654 | 655 | // First child span 656 | const firstChildSpan = spans[1]; 657 | expect(firstChildSpan.traceId).toBe(traceId); 658 | expect(firstChildSpan.parentSpanId).toBe(rootSpan.spanId); 659 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 660 | expect(firstChildSpan.endTimeUnixNano).not.toBe(0); 661 | expect(firstChildSpan.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.HTTP_HOST)) 662 | .toStrictEqual({ key: ATTRIBUTE_NAME.HTTP_HOST, value: { stringValue: 'example.com' } }); 663 | 664 | // Second child span 665 | const secondChildSpan = spans[2]; 666 | expect(secondChildSpan.traceId).toBe(traceId); 667 | // Validate this is a child of the first child 668 | expect(secondChildSpan.parentSpanId).toBe(firstChildSpan.spanId); 669 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 670 | expect(secondChildSpan.endTimeUnixNano).not.toBe(0); 671 | expect(secondChildSpan.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.KV_KEY)) 672 | .toStrictEqual({ key: ATTRIBUTE_NAME.KV_KEY, value: { stringValue: 'abc' } }); 673 | }); 674 | 675 | test('You can add a child to a child span with events', async () => { 676 | devWorker = await startWorker('test/scripts/otlp/span-span-events.ts', { 677 | kv: [ { binding: 'KV', id: '' } ], 678 | }); 679 | 680 | const res = await devWorker.fetch('http://worker/test'); 681 | 682 | expect(res.status).toBe(200); 683 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 684 | 685 | const traceId = res.headers.get('x-trace-id'); 686 | if (traceId === null) { 687 | expect(traceId).not.toBeNull(); 688 | return; 689 | } 690 | const trace = await getTrace(collectorWorker, traceId); 691 | 692 | // Root + 2 children 693 | expect(trace.resourceSpans.length).toBe(1); 694 | const resourceSpan = trace.resourceSpans[0]; 695 | 696 | expect(resourceSpan.scopeSpans.length).toBe(1); 697 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('span-span-events'); 698 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(3); 699 | 700 | const spans = resourceSpan.scopeSpans[0].spans; 701 | 702 | // Root span 703 | const rootSpan = spans[0]; 704 | expect(rootSpan.traceId).toBe(traceId); 705 | expect(rootSpan.name).toBe('Request (fetch event)'); 706 | expect(rootSpan.endTimeUnixNano).not.toBe(0); 707 | 708 | // First child span 709 | const firstChildSpan = spans[1]; 710 | expect(firstChildSpan.traceId).toBe(traceId); 711 | expect(firstChildSpan.parentSpanId).toBe(rootSpan.spanId); 712 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 713 | expect(firstChildSpan.endTimeUnixNano).not.toBe(0); 714 | 715 | // - Events 716 | expect(firstChildSpan.events.length).toBe(2); 717 | expect(firstChildSpan.events[0].name).toBe('Fetch done'); 718 | expect(firstChildSpan.events[0].timeUnixNano).not.toBe(0); 719 | expect(firstChildSpan.events[0].attributes.length).toBe(1); 720 | expect(firstChildSpan.events[0].attributes[0]) 721 | .toStrictEqual({ key: ATTRIBUTE_NAME.HTTP_HOST, value: { stringValue: 'example.com' } }); 722 | 723 | expect(firstChildSpan.events[1].name).toBe('Response body parsed'); 724 | expect(firstChildSpan.events[1].timeUnixNano).not.toBe(0); 725 | expect(firstChildSpan.events[1].attributes.length).toBe(1); 726 | expect(firstChildSpan.events[1].attributes[0]) 727 | .toStrictEqual({ key: 'parsed', value: { stringValue: 'text' } }); 728 | 729 | // Second child span 730 | const secondChildSpan = spans[2]; 731 | expect(secondChildSpan.traceId).toBe(traceId); 732 | // Validate this is a child of the first child 733 | expect(secondChildSpan.parentSpanId).toBe(firstChildSpan.spanId); 734 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 735 | expect(secondChildSpan.endTimeUnixNano).not.toBe(0); 736 | 737 | // - Events 738 | expect(secondChildSpan.events.length).toBe(1); 739 | expect(secondChildSpan.events[0].name).toBe('KV get done'); 740 | expect(secondChildSpan.events[0].timeUnixNano).not.toBe(0); 741 | expect(secondChildSpan.events[0].attributes.length).toBe(1); 742 | expect(secondChildSpan.events[0].attributes[0]) 743 | .toStrictEqual({ key: ATTRIBUTE_NAME.KV_KEY, value: { stringValue: 'abc' } }); 744 | }); 745 | 746 | test('You can add a child to a child span with attributes and events', async () => { 747 | devWorker = await startWorker('test/scripts/otlp/span-span-attributes-and-events.ts', { 748 | kv: [ { binding: 'KV', id: '' } ], 749 | }); 750 | 751 | const res = await devWorker.fetch('http://worker/test'); 752 | 753 | expect(res.status).toBe(200); 754 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 755 | 756 | const traceId = res.headers.get('x-trace-id'); 757 | if (traceId === null) { 758 | expect(traceId).not.toBeNull(); 759 | return; 760 | } 761 | const trace = await getTrace(collectorWorker, traceId); 762 | 763 | // Root + 2 children 764 | expect(trace.resourceSpans.length).toBe(1); 765 | const resourceSpan = trace.resourceSpans[0]; 766 | 767 | expect(resourceSpan.scopeSpans.length).toBe(1); 768 | expect(resourceSpan.scopeSpans[0].scope.name).toBe('span-span-attributes-and-events'); 769 | expect(resourceSpan.scopeSpans[0].spans.length).toBe(3); 770 | 771 | const spans = resourceSpan.scopeSpans[0].spans; 772 | 773 | // Root span 774 | const rootSpan = spans[0]; 775 | expect(rootSpan.traceId).toBe(traceId); 776 | expect(rootSpan.name).toBe('Request (fetch event)'); 777 | expect(rootSpan.endTimeUnixNano).not.toBe(0); 778 | 779 | // First child span 780 | const firstChildSpan = spans[1]; 781 | expect(firstChildSpan.traceId).toBe(traceId); 782 | expect(firstChildSpan.parentSpanId).toBe(rootSpan.spanId); 783 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 784 | expect(firstChildSpan.endTimeUnixNano).not.toBe(0); 785 | expect(firstChildSpan.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.HTTP_HOST)) 786 | .toStrictEqual({ key: ATTRIBUTE_NAME.HTTP_HOST, value: { stringValue: 'example.com' } }); 787 | 788 | // - Events 789 | expect(firstChildSpan.events.length).toBe(2); 790 | expect(firstChildSpan.events[0].name).toBe('Fetch done'); 791 | expect(firstChildSpan.events[0].timeUnixNano).not.toBe(0); 792 | expect(firstChildSpan.events[0].attributes.length).toBe(1); 793 | expect(firstChildSpan.events[0].attributes[0]) 794 | .toStrictEqual({ key: ATTRIBUTE_NAME.HTTP_HOST, value: { stringValue: 'example.com' } }); 795 | 796 | expect(firstChildSpan.events[1].name).toBe('Response body parsed'); 797 | expect(firstChildSpan.events[1].timeUnixNano).not.toBe(0); 798 | expect(firstChildSpan.events[1].attributes.length).toBe(1); 799 | expect(firstChildSpan.events[1].attributes[0]) 800 | .toStrictEqual({ key: 'parsed', value: { stringValue: 'text' } }); 801 | 802 | // Second child span 803 | const secondChildSpan = spans[2]; 804 | expect(secondChildSpan.traceId).toBe(traceId); 805 | // Validate this is a child of the first child 806 | expect(secondChildSpan.parentSpanId).toBe(firstChildSpan.spanId); 807 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 808 | expect(secondChildSpan.endTimeUnixNano).not.toBe(0); 809 | expect(secondChildSpan.attributes.find((attribute) => attribute.key === ATTRIBUTE_NAME.KV_KEY)) 810 | .toStrictEqual({ key: ATTRIBUTE_NAME.KV_KEY, value: { stringValue: 'abc' } }); 811 | 812 | // - Events 813 | expect(secondChildSpan.events.length).toBe(1); 814 | expect(secondChildSpan.events[0].name).toBe('KV get done'); 815 | expect(secondChildSpan.events[0].timeUnixNano).not.toBe(0); 816 | expect(secondChildSpan.events[0].attributes.length).toBe(1); 817 | expect(secondChildSpan.events[0].attributes[0]) 818 | .toStrictEqual({ key: ATTRIBUTE_NAME.KV_KEY, value: { stringValue: 'abc' } }); 819 | }); 820 | }); 821 | }); 822 | -------------------------------------------------------------------------------- /test/scripts/api/root-span-attributes.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | 3 | interface Env {} 4 | 5 | export default { 6 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 7 | const trace = createTrace(req, env, ctx, { 8 | serviceName: 'root-span-attributes', 9 | collector: { 10 | url: 'http://0.0.0.0:4318/v1/traces', 11 | }, 12 | }, { 13 | attributes: { 14 | customAttribute: 1337, 15 | workersTracing: true, 16 | }, 17 | }); 18 | 19 | await trace.send(); 20 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /test/scripts/api/root-span-events.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | 3 | interface Env {} 4 | 5 | export default { 6 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 7 | const trace = createTrace(req, env, ctx, { 8 | serviceName: 'root-span-events', 9 | collector: { 10 | url: 'http://0.0.0.0:4318/v1/traces', 11 | }, 12 | }); 13 | 14 | await fetch('https://example.com'); 15 | trace.addEvent({ name: 'Fetch done', timestamp: Date.now() }); 16 | 17 | await trace.send(); 18 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /test/scripts/api/root-span-links.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | 3 | interface Env {} 4 | 5 | export default { 6 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 7 | const trace = createTrace(req, env, ctx, { 8 | serviceName: 'root-span-links', 9 | collector: { 10 | url: 'http://0.0.0.0:4318/v1/traces', 11 | }, 12 | }, { 13 | links: [ 14 | { 15 | context: { traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', spanId: 'bbbbbbbbbbbbbbbb' }, 16 | attributes: {}, 17 | }, 18 | { 19 | context: { traceId: 'cccccccccccccccccccccccccccccccc', spanId: 'dddddddddddddddd' }, 20 | attributes: { 21 | link: 2, 22 | muchWow: true, 23 | }, 24 | }, 25 | ], 26 | }); 27 | 28 | await trace.send(); 29 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /test/scripts/api/root-span-resource-attributes.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ATTRIBUTE_NAME } from 'src/utils/constants'; 3 | 4 | interface Env {} 5 | 6 | export default { 7 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 8 | const trace = createTrace(req, env, ctx, { 9 | serviceName: 'root-span-resource-attributes', 10 | collector: { 11 | url: 'http://0.0.0.0:4318/v1/traces', 12 | }, 13 | resource: { 14 | attributes: { 15 | example: true, 16 | [ATTRIBUTE_NAME.RUNTIME_NAME]: 'blob-runtime', 17 | }, 18 | }, 19 | }); 20 | 21 | await trace.send(); 22 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /test/scripts/api/root-span-status.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { StatusCode } from 'src/tracing'; 3 | 4 | interface Env {} 5 | 6 | export default { 7 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 8 | const trace = createTrace(req, env, ctx, { 9 | serviceName: 'root-span-status', 10 | collector: { 11 | url: 'http://0.0.0.0:4318/v1/traces', 12 | }, 13 | }); 14 | 15 | trace.setStatus(StatusCode.OK); 16 | await trace.send(); 17 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /test/scripts/api/root-span.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | 3 | interface Env {} 4 | 5 | export default { 6 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 7 | const trace = createTrace(req, env, ctx, { 8 | serviceName: 'root-span', 9 | collector: { 10 | url: 'http://0.0.0.0:4318/v1/traces', 11 | }, 12 | }); 13 | 14 | await trace.send(); 15 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /test/scripts/api/span-builder/add-remove-attributes.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | 3 | interface Env {} 4 | 5 | export default { 6 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 7 | const trace = createTrace(req, env, ctx, { 8 | serviceName: 'span-builder-add-remove-attributes', 9 | collector: { 10 | url: 'http://0.0.0.0:4318/v1/traces', 11 | }, 12 | }); 13 | 14 | // string | number | boolean | string[] | number[] | boolean[] 15 | const span = trace.buildSpan('fetch') 16 | .addAttribute('str', 'example.com') 17 | .addAttribute('int', 1337); 18 | 19 | await fetch('https://example.com'); 20 | 21 | span.removeAttribute('str'); 22 | 23 | span.end(); 24 | await trace.send(); 25 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /test/scripts/api/span-builder/attributes.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | 3 | interface Env {} 4 | 5 | export default { 6 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 7 | const trace = createTrace(req, env, ctx, { 8 | serviceName: 'span-builder-attributes', 9 | collector: { 10 | url: 'http://0.0.0.0:4318/v1/traces', 11 | }, 12 | }); 13 | 14 | // string | number | boolean | string[] | number[] | boolean[] 15 | const span = trace.buildSpan('fetch') 16 | .addAttribute('str', 'example.com') 17 | .addAttribute('int', 1337) 18 | .addAttribute('double', 13.37) 19 | .addAttribute('bool', true) 20 | .addAttribute('strArray', ['a', 'b', 'c']) 21 | .addAttribute('intArray', [1, 2, 3]) 22 | .addAttribute('doubleArray', [1.1, 2.2, 3.3]) 23 | .addAttribute('boolArray', [true, false, true]); 24 | 25 | await fetch('https://example.com'); 26 | 27 | span.end(); 28 | await trace.send(); 29 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /test/scripts/api/span-builder/basic.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | 3 | interface Env {} 4 | 5 | export default { 6 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 7 | const trace = createTrace(req, env, ctx, { 8 | serviceName: 'span-builder-basic', 9 | collector: { 10 | url: 'http://0.0.0.0:4318/v1/traces', 11 | }, 12 | }); 13 | 14 | const span = trace.buildSpan('fetch'); 15 | 16 | await fetch('https://example.com'); 17 | 18 | span.end(); 19 | await trace.send(); 20 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /test/scripts/api/span-builder/event.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | 3 | interface Env {} 4 | 5 | export default { 6 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 7 | const trace = createTrace(req, env, ctx, { 8 | serviceName: 'span-builder-event', 9 | collector: { 10 | url: 'http://0.0.0.0:4318/v1/traces', 11 | }, 12 | }); 13 | 14 | const span = trace.buildSpan('fetch') 15 | .addEvent('Span started'); 16 | 17 | await fetch('https://example.com'); 18 | span.addEvent('Fetch done', { host: 'example.com' }); 19 | 20 | span.end(); 21 | await trace.send(); 22 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /test/scripts/api/span-builder/links.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | 3 | interface Env {} 4 | 5 | export default { 6 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 7 | const trace = createTrace(req, env, ctx, { 8 | serviceName: 'span-builder-links', 9 | collector: { 10 | url: 'http://0.0.0.0:4318/v1/traces', 11 | }, 12 | }); 13 | 14 | const span = trace.buildSpan('fetch') 15 | .addLink(trace.getContext()); 16 | 17 | await fetch('https://example.com'); 18 | span.addLink( 19 | { traceId: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', spanId: 'aaaaaaaaaaaaaaaa' }, 20 | { service: 'example' }, 21 | ); 22 | 23 | span.end(); 24 | await trace.send(); 25 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /test/scripts/api/span-builder/status.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { StatusCode } from 'src/tracing'; 3 | 4 | interface Env {} 5 | 6 | export default { 7 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 8 | const trace = createTrace(req, env, ctx, { 9 | serviceName: 'span-builder-status', 10 | collector: { 11 | url: 'http://0.0.0.0:4318/v1/traces', 12 | }, 13 | }); 14 | 15 | const span = trace.buildSpan('fetch') 16 | .setStatus(StatusCode.UNSET); 17 | 18 | try { 19 | await fetch('https://example.com'); 20 | span.setStatus(StatusCode.OK); 21 | } catch(e) { 22 | if (e instanceof Error) { 23 | span.setStatus(StatusCode.ERROR, e.message); 24 | } else { 25 | span.setStatus(StatusCode.ERROR, 'Unknown error'); 26 | } 27 | } 28 | 29 | span.end(); 30 | await trace.send(); 31 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 32 | }, 33 | }; 34 | -------------------------------------------------------------------------------- /test/scripts/collector.ts: -------------------------------------------------------------------------------- 1 | // This Worker is responsible for collecting traces so that we can validate them 2 | 3 | interface Env { 4 | KV: KVNamespace; 5 | } 6 | 7 | export default { 8 | async fetch(req: Request, env: Env) { 9 | const { pathname } = new URL(req.url); 10 | 11 | if ( 12 | pathname === '/v1/traces' // Jaeger OTLP 13 | || pathname === '/api/v2/spans' // Jaeger Zipkin 14 | ) { 15 | // Validation 16 | if (req.headers.get('content-type') !== 'application/json') { 17 | return Response.json({ error: '"content-type: application/json" is required' }, { status: 400 }); 18 | } 19 | 20 | const body = await req.json(); 21 | 22 | const traceId = req.headers.get('x-trace-id'); 23 | if (traceId !== null) { 24 | await env.KV.put(traceId, JSON.stringify(body)); 25 | } else { 26 | return Response.json({ error: 'No x-trace-id header provided' }, { status: 400 }); 27 | } 28 | 29 | return Response.json({ message: 'Trace ingested' }); 30 | 31 | } else if (pathname.startsWith('/__/lookup/')) { 32 | const traceId = pathname.replace('/__/lookup/', ''); 33 | 34 | const value = await env.KV.get(traceId, 'json'); 35 | if (value === null) { 36 | return Response.json({ error: 'Trace not found' }, { status: 404 }); 37 | } 38 | 39 | return Response.json(value); 40 | } 41 | 42 | return Response.json({ error: 'Not found' }, { status: 404 }); 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /test/scripts/otlp/basic.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | 4 | interface Env {} 5 | 6 | export default { 7 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 8 | const trace = createTrace(req, env, ctx, { 9 | serviceName: 'otlp-basic', 10 | collector: { 11 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 12 | transformer: new OtlpTransformer(), 13 | }, 14 | }); 15 | 16 | await trace.send(); 17 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /test/scripts/otlp/multiple-spans-attributes-and-events.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'multiple-spans-attributes-and-events', 13 | collector: { 14 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 15 | transformer: new OtlpTransformer(), 16 | }, 17 | }); 18 | 19 | const fetchSpan = trace.startSpan(SPAN_NAME.FETCH, { 20 | attributes: { [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com' }, 21 | }); 22 | 23 | const res = await fetch('https://example.com'); 24 | fetchSpan.addEvent({ 25 | name: 'Fetch done', 26 | timestamp: Date.now(), 27 | attributes: { 28 | [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com', 29 | }, 30 | }); 31 | 32 | await res.text(); 33 | fetchSpan.addEvent({ 34 | name: 'Response body parsed', 35 | timestamp: Date.now(), 36 | attributes: { 37 | parsed: 'text', 38 | }, 39 | }); 40 | fetchSpan.end(); 41 | 42 | const kvSpan = trace.startSpan(SPAN_NAME.KV_GET, { 43 | attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc' }, 44 | }); 45 | 46 | await env.KV.get('abc'); 47 | kvSpan.addEvent({ 48 | name: 'KV get done', 49 | timestamp: Date.now(), 50 | attributes: { 51 | [ATTRIBUTE_NAME.KV_KEY]: 'abc', 52 | }, 53 | }); 54 | kvSpan.end(); 55 | 56 | await trace.send(); 57 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /test/scripts/otlp/multiple-spans-attributes.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'multiple-spans-attributes', 13 | collector: { 14 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 15 | transformer: new OtlpTransformer(), 16 | }, 17 | }); 18 | 19 | await trace.trace(SPAN_NAME.FETCH, () => fetch('https://example.com'), { 20 | attributes: { [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com' }, 21 | }); 22 | 23 | await trace.trace(SPAN_NAME.KV_GET, () => env.KV.get('abc'), { 24 | attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc' }, 25 | }); 26 | 27 | await trace.send(); 28 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /test/scripts/otlp/multiple-spans-events.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'multiple-spans-events', 13 | collector: { 14 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 15 | transformer: new OtlpTransformer(), 16 | }, 17 | }); 18 | 19 | const fetchSpan = trace.startSpan(SPAN_NAME.FETCH); 20 | 21 | const res = await fetch('https://example.com'); 22 | fetchSpan.addEvent({ 23 | name: 'Fetch done', 24 | timestamp: Date.now(), 25 | attributes: { 26 | [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com', 27 | }, 28 | }); 29 | 30 | await res.text(); 31 | fetchSpan.addEvent({ 32 | name: 'Response body parsed', 33 | timestamp: Date.now(), 34 | attributes: { 35 | parsed: 'text', 36 | }, 37 | }); 38 | fetchSpan.end(); 39 | 40 | const kvSpan = trace.startSpan(SPAN_NAME.KV_GET); 41 | await env.KV.get('abc'); 42 | kvSpan.addEvent({ 43 | name: 'KV get done', 44 | timestamp: Date.now(), 45 | attributes: { 46 | [ATTRIBUTE_NAME.KV_KEY]: 'abc', 47 | }, 48 | }); 49 | kvSpan.end(); 50 | 51 | await trace.send(); 52 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /test/scripts/otlp/multiple-spans.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | import { SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'multiple-spans', 13 | collector: { 14 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 15 | transformer: new OtlpTransformer(), 16 | }, 17 | }); 18 | 19 | await trace.trace(SPAN_NAME.FETCH, () => fetch('https://example.com')); 20 | await trace.trace(SPAN_NAME.KV_GET, () => env.KV.get('abc')); 21 | 22 | await trace.send(); 23 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /test/scripts/otlp/resource-attributes.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | import { ATTRIBUTE_NAME } from 'src/utils/constants'; 4 | 5 | interface Env {} 6 | 7 | export default { 8 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 9 | const trace = createTrace(req, env, ctx, { 10 | serviceName: 'resource-attributes', 11 | collector: { 12 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 13 | transformer: new OtlpTransformer(), 14 | }, 15 | resource: { 16 | attributes: { 17 | exampleAttribute: true, 18 | [ATTRIBUTE_NAME.RUNTIME_NAME]: 'blob-runtime', 19 | }, 20 | }, 21 | }); 22 | 23 | await trace.send(); 24 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /test/scripts/otlp/single-span-attributes-and-events.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env {} 6 | 7 | export default { 8 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 9 | const trace = createTrace(req, env, ctx, { 10 | serviceName: 'single-span-attributes-and-events', 11 | collector: { 12 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 13 | transformer: new OtlpTransformer(), 14 | }, 15 | }); 16 | 17 | const span = trace.startSpan(SPAN_NAME.FETCH, { 18 | attributes: { [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com' }, 19 | }); 20 | 21 | const res = await fetch('https://example.com'); 22 | span.addEvent({ 23 | name: 'Fetch done', 24 | timestamp: Date.now(), 25 | attributes: { 26 | [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com', 27 | }, 28 | }); 29 | 30 | await res.text(); 31 | span.addEvent({ 32 | name: 'Response body parsed', 33 | timestamp: Date.now(), 34 | attributes: { 35 | parsed: 'text', 36 | }, 37 | }); 38 | 39 | span.end(); 40 | await trace.send(); 41 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /test/scripts/otlp/single-span-attributes.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env {} 6 | 7 | export default { 8 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 9 | const trace = createTrace(req, env, ctx, { 10 | serviceName: 'single-span-attributes', 11 | collector: { 12 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 13 | transformer: new OtlpTransformer(), 14 | }, 15 | }); 16 | 17 | await trace.trace(SPAN_NAME.FETCH, () => fetch('https://example.com'), { 18 | attributes: { [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com' }, 19 | }); 20 | 21 | await trace.send(); 22 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /test/scripts/otlp/single-span-events.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env {} 6 | 7 | export default { 8 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 9 | const trace = createTrace(req, env, ctx, { 10 | serviceName: 'single-span-events', 11 | collector: { 12 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 13 | transformer: new OtlpTransformer(), 14 | }, 15 | }); 16 | 17 | const span = trace.startSpan(SPAN_NAME.FETCH); 18 | 19 | const res = await fetch('https://example.com'); 20 | span.addEvent({ 21 | name: 'Fetch done', 22 | timestamp: Date.now(), 23 | attributes: { 24 | [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com', 25 | }, 26 | }); 27 | 28 | await res.text(); 29 | span.addEvent({ 30 | name: 'Response body parsed', 31 | timestamp: Date.now(), 32 | attributes: { 33 | parsed: 'text', 34 | }, 35 | }); 36 | 37 | span.end(); 38 | await trace.send(); 39 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /test/scripts/otlp/single-span.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | import { SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env {} 6 | 7 | export default { 8 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 9 | const trace = createTrace(req, env, ctx, { 10 | serviceName: 'single-span', 11 | collector: { 12 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 13 | transformer: new OtlpTransformer(), 14 | }, 15 | }); 16 | 17 | await trace.trace(SPAN_NAME.FETCH, () => fetch('https://example.com')); 18 | 19 | await trace.send(); 20 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /test/scripts/otlp/span-span-attributes-and-events.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'span-span-attributes-and-events', 13 | collector: { 14 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 15 | transformer: new OtlpTransformer(), 16 | }, 17 | }); 18 | 19 | const fetchSpan = trace.startSpan(SPAN_NAME.FETCH, { 20 | attributes: { [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com' }, 21 | }); 22 | 23 | const res = await fetch('https://example.com'); 24 | fetchSpan.addEvent({ 25 | name: 'Fetch done', 26 | timestamp: Date.now(), 27 | attributes: { 28 | [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com', 29 | }, 30 | }); 31 | 32 | await res.text(); 33 | fetchSpan.addEvent({ 34 | name: 'Response body parsed', 35 | timestamp: Date.now(), 36 | attributes: { 37 | parsed: 'text', 38 | }, 39 | }); 40 | 41 | const kvSpan = fetchSpan.startSpan(SPAN_NAME.KV_GET, { 42 | attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc' }, 43 | }); 44 | 45 | await env.KV.get('abc'); 46 | kvSpan.addEvent({ 47 | name: 'KV get done', 48 | timestamp: Date.now(), 49 | attributes: { 50 | [ATTRIBUTE_NAME.KV_KEY]: 'abc', 51 | }, 52 | }); 53 | kvSpan.end(); 54 | fetchSpan.end(); 55 | 56 | await trace.send(); 57 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /test/scripts/otlp/span-span-attributes.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'span-span-attributes', 13 | collector: { 14 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 15 | transformer: new OtlpTransformer(), 16 | }, 17 | }); 18 | 19 | const fetchSpan = trace.startSpan(SPAN_NAME.FETCH, { 20 | attributes: { [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com' }, 21 | }); 22 | 23 | await fetch('https://example.com'); 24 | 25 | const kvSpan = fetchSpan.startSpan(SPAN_NAME.KV_GET, { 26 | attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc' }, 27 | }); 28 | 29 | await env.KV.get('abc'); 30 | kvSpan.end(); 31 | fetchSpan.end(); 32 | 33 | await trace.send(); 34 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /test/scripts/otlp/span-span-events.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'span-span-events', 13 | collector: { 14 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 15 | transformer: new OtlpTransformer(), 16 | }, 17 | }); 18 | 19 | const fetchSpan = trace.startSpan(SPAN_NAME.FETCH); 20 | 21 | const res = await fetch('https://example.com'); 22 | fetchSpan.addEvent({ 23 | name: 'Fetch done', 24 | timestamp: Date.now(), 25 | attributes: { 26 | [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com', 27 | }, 28 | }); 29 | 30 | await res.text(); 31 | fetchSpan.addEvent({ 32 | name: 'Response body parsed', 33 | timestamp: Date.now(), 34 | attributes: { 35 | parsed: 'text', 36 | }, 37 | }); 38 | 39 | const kvSpan = fetchSpan.startSpan(SPAN_NAME.KV_GET); 40 | await env.KV.get('abc'); 41 | kvSpan.addEvent({ 42 | name: 'KV get done', 43 | timestamp: Date.now(), 44 | attributes: { 45 | [ATTRIBUTE_NAME.KV_KEY]: 'abc', 46 | }, 47 | }); 48 | kvSpan.end(); 49 | fetchSpan.end(); 50 | 51 | await trace.send(); 52 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /test/scripts/otlp/span-span.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { OtlpTransformer } from 'src/transformers/otlp'; 3 | import { SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'span-span', 13 | collector: { 14 | url: 'http://0.0.0.0:4318/v1/traces', // OTLP compatible Jaeger endpoint 15 | transformer: new OtlpTransformer(), 16 | }, 17 | }); 18 | 19 | const fetchSpan = trace.startSpan(SPAN_NAME.FETCH); 20 | await fetch('https://example.com'); 21 | 22 | const kvSpan = fetchSpan.startSpan(SPAN_NAME.KV_GET); 23 | await env.KV.get('abc'); 24 | kvSpan.end(); 25 | fetchSpan.end(); 26 | 27 | await trace.send(); 28 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /test/scripts/zipkin/basic.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 3 | 4 | interface Env {} 5 | 6 | export default { 7 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 8 | const trace = createTrace(req, env, ctx, { 9 | serviceName: 'zipkin-basic', 10 | collector: { 11 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 12 | transformer: new ZipkinTransformer(), 13 | }, 14 | }); 15 | 16 | await trace.send(); 17 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /test/scripts/zipkin/multiple-spans-attributes-and-events.ts: -------------------------------------------------------------------------------- 1 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 2 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 3 | import { createTrace } from 'src/trace'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'multiple-spans-attributes-and-events', 13 | collector: { 14 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 15 | transformer: new ZipkinTransformer(), 16 | }, 17 | }); 18 | 19 | const fetchSpan = trace.startSpan(SPAN_NAME.FETCH, { 20 | attributes: { [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com' }, 21 | }); 22 | 23 | const res = await fetch('https://example.com'); 24 | fetchSpan.addEvent({ name: 'Fetch done', timestamp: Date.now() }); 25 | 26 | await res.text(); 27 | fetchSpan.addEvent({ name: 'Response body parsed', timestamp: Date.now() }); 28 | fetchSpan.end(); 29 | 30 | const kvSpan = trace.startSpan(SPAN_NAME.KV_GET, { 31 | attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc' }, 32 | }); 33 | 34 | await env.KV.get('abc'); 35 | kvSpan.addEvent({ name: 'KV get done', timestamp: Date.now() }); 36 | kvSpan.end(); 37 | 38 | await trace.send(); 39 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /test/scripts/zipkin/multiple-spans-attributes.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'multiple-spans-attributes', 13 | collector: { 14 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 15 | transformer: new ZipkinTransformer(), 16 | }, 17 | }); 18 | 19 | await trace.trace(SPAN_NAME.FETCH, () => fetch('https://example.com'), { 20 | attributes: { [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com' }, 21 | }); 22 | 23 | await trace.trace(SPAN_NAME.KV_GET, () => env.KV.get('abc'), { 24 | attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc' }, 25 | }); 26 | 27 | await trace.send(); 28 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /test/scripts/zipkin/multiple-spans-events.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 3 | import { SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'multiple-spans-events', 13 | collector: { 14 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 15 | transformer: new ZipkinTransformer(), 16 | }, 17 | }); 18 | 19 | const fetchSpan = trace.startSpan(SPAN_NAME.FETCH); 20 | 21 | const res = await fetch('https://example.com'); 22 | fetchSpan.addEvent({ name: 'Fetch done', timestamp: Date.now() }); 23 | 24 | await res.text(); 25 | fetchSpan.addEvent({ name: 'Response body parsed', timestamp: Date.now() }); 26 | fetchSpan.end(); 27 | 28 | const kvSpan = trace.startSpan(SPAN_NAME.KV_GET); 29 | await env.KV.get('abc'); 30 | kvSpan.addEvent({ name: 'KV get done', timestamp: Date.now() }); 31 | kvSpan.end(); 32 | 33 | await trace.send(); 34 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /test/scripts/zipkin/multiple-spans.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 3 | import { SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'multiple-spans', 13 | collector: { 14 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 15 | transformer: new ZipkinTransformer(), 16 | }, 17 | }); 18 | 19 | await trace.trace(SPAN_NAME.FETCH, () => fetch('https://example.com')); 20 | await trace.trace(SPAN_NAME.KV_GET, () => env.KV.get('abc')); 21 | 22 | await trace.send(); 23 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /test/scripts/zipkin/resource-attributes.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 3 | import { ATTRIBUTE_NAME } from 'src/utils/constants'; 4 | 5 | interface Env {} 6 | 7 | export default { 8 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 9 | const trace = createTrace(req, env, ctx, { 10 | serviceName: 'resource-attributes', 11 | collector: { 12 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 13 | transformer: new ZipkinTransformer(), 14 | }, 15 | resource: { 16 | attributes: { 17 | exampleAttribute: true, 18 | [ATTRIBUTE_NAME.RUNTIME_NAME]: 'blob-runtime', 19 | }, 20 | }, 21 | }); 22 | 23 | await trace.send(); 24 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /test/scripts/zipkin/single-span-attributes-and-events.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env {} 6 | 7 | export default { 8 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 9 | const trace = createTrace(req, env, ctx, { 10 | serviceName: 'single-span-attributes-and-events', 11 | collector: { 12 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 13 | transformer: new ZipkinTransformer(), 14 | }, 15 | }); 16 | 17 | const span = trace.startSpan(SPAN_NAME.FETCH, { 18 | attributes: { [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com' }, 19 | }); 20 | 21 | const res = await fetch('https://example.com'); 22 | span.addEvent({ name: 'Fetch done', timestamp: Date.now() }); 23 | 24 | await res.text(); 25 | span.addEvent({ name: 'Response body parsed', timestamp: Date.now() }); 26 | 27 | span.end(); 28 | await trace.send(); 29 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /test/scripts/zipkin/single-span-attributes.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env {} 6 | 7 | export default { 8 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 9 | const trace = createTrace(req, env, ctx, { 10 | serviceName: 'single-span-attributes', 11 | collector: { 12 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 13 | transformer: new ZipkinTransformer(), 14 | }, 15 | }); 16 | 17 | await trace.trace(SPAN_NAME.FETCH, () => fetch('https://example.com'), { 18 | attributes: { [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com' }, 19 | }); 20 | 21 | await trace.send(); 22 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /test/scripts/zipkin/single-span-events.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 3 | import { SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env {} 6 | 7 | export default { 8 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 9 | const trace = createTrace(req, env, ctx, { 10 | serviceName: 'single-span-events', 11 | collector: { 12 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 13 | transformer: new ZipkinTransformer(), 14 | }, 15 | }); 16 | 17 | const span = trace.startSpan(SPAN_NAME.FETCH); 18 | 19 | const res = await fetch('https://example.com'); 20 | span.addEvent({ name: 'Fetch done', timestamp: Date.now() }); 21 | 22 | await res.text(); 23 | span.addEvent({ name: 'Response body parsed', timestamp: Date.now() }); 24 | 25 | span.end(); 26 | await trace.send(); 27 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /test/scripts/zipkin/single-span.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 3 | import { SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env {} 6 | 7 | export default { 8 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 9 | const trace = createTrace(req, env, ctx, { 10 | serviceName: 'single-span', 11 | collector: { 12 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 13 | transformer: new ZipkinTransformer(), 14 | }, 15 | }); 16 | 17 | await trace.trace(SPAN_NAME.FETCH, () => fetch('https://example.com')); 18 | 19 | await trace.send(); 20 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /test/scripts/zipkin/span-span-attributes-and-events.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'span-span-attributes-and-events', 13 | collector: { 14 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 15 | transformer: new ZipkinTransformer(), 16 | }, 17 | }); 18 | 19 | const fetchSpan = trace.startSpan(SPAN_NAME.FETCH, { 20 | attributes: { [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com' }, 21 | }); 22 | 23 | const res = await fetch('https://example.com'); 24 | fetchSpan.addEvent({ name: 'Fetch done', timestamp: Date.now() }); 25 | 26 | await res.text(); 27 | fetchSpan.addEvent({ name: 'Response body parsed', timestamp: Date.now() }); 28 | 29 | const kvSpan = fetchSpan.startSpan(SPAN_NAME.KV_GET, { 30 | attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc' }, 31 | }); 32 | 33 | await env.KV.get('abc'); 34 | kvSpan.addEvent({ name: 'KV get done', timestamp: Date.now() }); 35 | kvSpan.end(); 36 | fetchSpan.end(); 37 | 38 | await trace.send(); 39 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 40 | }, 41 | }; 42 | -------------------------------------------------------------------------------- /test/scripts/zipkin/span-span-attributes.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 3 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'span-span-attributes', 13 | collector: { 14 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 15 | transformer: new ZipkinTransformer(), 16 | }, 17 | }); 18 | 19 | const fetchSpan = trace.startSpan(SPAN_NAME.FETCH, { 20 | attributes: { [ATTRIBUTE_NAME.HTTP_HOST]: 'example.com' }, 21 | }); 22 | 23 | await fetch('https://example.com'); 24 | 25 | const kvSpan = fetchSpan.startSpan(SPAN_NAME.KV_GET, { 26 | attributes: { [ATTRIBUTE_NAME.KV_KEY]: 'abc' }, 27 | }); 28 | 29 | await env.KV.get('abc'); 30 | kvSpan.end(); 31 | fetchSpan.end(); 32 | 33 | await trace.send(); 34 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /test/scripts/zipkin/span-span-events.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 3 | import { SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'span-span-events', 13 | collector: { 14 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 15 | transformer: new ZipkinTransformer(), 16 | }, 17 | }); 18 | 19 | const fetchSpan = trace.startSpan(SPAN_NAME.FETCH); 20 | 21 | const res = await fetch('https://example.com'); 22 | fetchSpan.addEvent({ name: 'Fetch done', timestamp: Date.now() }); 23 | 24 | await res.text(); 25 | fetchSpan.addEvent({ name: 'Response body parsed', timestamp: Date.now() }); 26 | 27 | const kvSpan = fetchSpan.startSpan(SPAN_NAME.KV_GET); 28 | await env.KV.get('abc'); 29 | kvSpan.addEvent({ name: 'KV get done', timestamp: Date.now() }); 30 | kvSpan.end(); 31 | fetchSpan.end(); 32 | 33 | await trace.send(); 34 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 35 | }, 36 | }; 37 | -------------------------------------------------------------------------------- /test/scripts/zipkin/span-span.ts: -------------------------------------------------------------------------------- 1 | import { createTrace } from 'src/trace'; 2 | import { ZipkinTransformer } from 'src/transformers/zipkin'; 3 | import { SPAN_NAME } from 'src/utils/constants'; 4 | 5 | interface Env { 6 | KV: KVNamespace; 7 | } 8 | 9 | export default { 10 | async fetch(req: Request, env: Env, ctx: ExecutionContext) { 11 | const trace = createTrace(req, env, ctx, { 12 | serviceName: 'span-span', 13 | collector: { 14 | url: 'http://0.0.0.0:9411/api/v2/spans', // Zipkin compatible Jaeger endpoint 15 | transformer: new ZipkinTransformer(), 16 | }, 17 | }); 18 | 19 | const fetchSpan = trace.startSpan(SPAN_NAME.FETCH); 20 | await fetch('https://example.com'); 21 | 22 | const kvSpan = fetchSpan.startSpan(SPAN_NAME.KV_GET); 23 | await env.KV.get('abc'); 24 | kvSpan.end(); 25 | fetchSpan.end(); 26 | 27 | await trace.send(); 28 | return new Response('ok', { headers: { 'x-trace-id': trace.getTraceId() } }); 29 | }, 30 | }; 31 | -------------------------------------------------------------------------------- /test/utils/trace.ts: -------------------------------------------------------------------------------- 1 | import { expect } from 'vitest'; 2 | import { UnstableDevWorker } from 'wrangler'; 3 | import { OtlpJson } from '../../src/transformers/otlp'; 4 | 5 | export async function requestAndGetTrace( 6 | devWorker: UnstableDevWorker, 7 | collectorWorker: UnstableDevWorker, 8 | url: string, 9 | ): Promise { 10 | const res = await devWorker.fetch(url); 11 | 12 | expect(res.status).toBe(200); 13 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 14 | 15 | const traceId = res.headers.get('x-trace-id'); 16 | expect(traceId).not.toBeNull(); 17 | 18 | // This is already expected above 19 | // eslint-disable-next-line @typescript-eslint/no-non-null-assertion 20 | const trace = await getTrace(collectorWorker, traceId!); 21 | 22 | return trace; 23 | } 24 | 25 | export async function getTrace( 26 | collectorWorker: UnstableDevWorker, 27 | traceId: string, 28 | ) { 29 | const collectorRes = await collectorWorker.fetch(`https://collector/__/lookup/${traceId}`); 30 | 31 | return collectorRes.json() as T; 32 | } 33 | -------------------------------------------------------------------------------- /test/utils/worker.ts: -------------------------------------------------------------------------------- 1 | import { UnstableDevWorker, unstable_dev } from 'wrangler'; 2 | 3 | interface DevOptions { 4 | bundle?: boolean; 5 | compatibilityDate?: string; 6 | compatibilityFlags?: string[]; 7 | persist?: boolean; 8 | persistTo?: string; 9 | vars?: { 10 | [key: string]: unknown; 11 | }; 12 | kv?: { 13 | binding: string; 14 | id: string; 15 | preview_id?: string; 16 | }[]; 17 | durableObjects?: { 18 | name: string; 19 | class_name: string; 20 | script_name?: string | undefined; 21 | environment?: string | undefined; 22 | }[]; 23 | r2?: { 24 | binding: string; 25 | bucket_name: string; 26 | preview_bucket_name?: string; 27 | }[]; 28 | d1Databases?: { 29 | binding: string; 30 | database_name: string; 31 | database_id: string; 32 | preview_database_id?: string; 33 | migrations_table?: string; 34 | migrations_dir?: string; 35 | database_internal_env?: string; 36 | }[]; 37 | showInteractiveDevSession?: boolean; 38 | logLevel?: 'none' | 'info' | 'error' | 'log' | 'warn' | 'debug'; 39 | logPrefix?: string; 40 | } 41 | 42 | // If there's errors in tests, worth changing this to "debug" 43 | const LOG_LEVEL: 'none' | 'info' | 'error' | 'log' | 'warn' | 'debug' = 'error'; 44 | 45 | export async function startWorker(scriptPath: string, opts?: DevOptions): Promise { 46 | if (LOG_LEVEL === 'debug') { 47 | console.log(`Starting ${scriptPath}`); 48 | } 49 | const worker = await unstable_dev(scriptPath, { 50 | bundle: true, 51 | local: true, 52 | logLevel: LOG_LEVEL, 53 | ...opts, 54 | }); 55 | 56 | if (LOG_LEVEL === 'debug') { 57 | console.log(`Started ${scriptPath} - address: ${worker.address}, port: ${worker.port}`); 58 | } 59 | 60 | return worker; 61 | } 62 | 63 | export async function startCollector(opts: DevOptions & { port: number }): Promise { 64 | if (LOG_LEVEL === 'debug') { 65 | console.log('Starting collector'); 66 | } 67 | const worker = await unstable_dev('test/scripts/collector.ts', { 68 | bundle: true, 69 | local: true, 70 | kv: [{ 71 | binding: 'KV', 72 | id: 'collector', 73 | }], 74 | logLevel: LOG_LEVEL, 75 | ...opts, 76 | }); 77 | 78 | if (LOG_LEVEL === 'debug') { 79 | console.log(`Started collector - address: ${worker.address}, port: ${worker.port}`); 80 | } 81 | 82 | return worker; 83 | } 84 | -------------------------------------------------------------------------------- /test/zipkin-exporter.test.ts: -------------------------------------------------------------------------------- 1 | import { ZipkinJson } from 'src/transformers/zipkin'; 2 | import { ATTRIBUTE_NAME, SPAN_NAME } from 'src/utils/constants'; 3 | import { afterAll, afterEach, beforeAll, describe, expect, test } from 'vitest'; 4 | import { UnstableDevWorker } from 'wrangler'; 5 | import { getTrace } from './utils/trace'; 6 | import { startCollector, startWorker } from './utils/worker'; 7 | 8 | let devWorker: UnstableDevWorker; 9 | let collectorWorker: UnstableDevWorker; 10 | 11 | describe('Test Zipkin Exporter', () => { 12 | beforeAll(async () => { 13 | collectorWorker = await startCollector({ port: 9411 }); 14 | }); 15 | 16 | afterEach(async () => { 17 | if (devWorker) { 18 | await devWorker.stop(); 19 | await devWorker.waitUntilExit(); 20 | } 21 | }); 22 | 23 | afterAll(async () => { 24 | if (collectorWorker) { 25 | await collectorWorker.stop(); 26 | await collectorWorker.waitUntilExit(); 27 | } 28 | }); 29 | 30 | test('Basic trace should transform correctly', async () => { 31 | devWorker = await startWorker('test/scripts/zipkin/basic.ts'); 32 | 33 | const res = await devWorker.fetch('http://worker/test'); 34 | 35 | expect(res.status).toBe(200); 36 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 37 | 38 | const traceId = res.headers.get('x-trace-id'); 39 | if (traceId === null) { 40 | expect(traceId).not.toBeNull(); 41 | return; 42 | } 43 | const trace = await getTrace(collectorWorker, traceId); 44 | 45 | const span = trace[0]; 46 | expect(span.traceId).toBe(traceId); 47 | expect(span.name).toBe('Request (fetch event)'); 48 | expect(span.localEndpoint.serviceName).toBe('zipkin-basic'); 49 | }); 50 | 51 | describe('Root span', () => { 52 | test('Default attributes are put on root span', async () => { 53 | devWorker = await startWorker('test/scripts/zipkin/basic.ts'); 54 | 55 | const res = await devWorker.fetch('http://worker/test'); 56 | 57 | expect(res.status).toBe(200); 58 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 59 | 60 | const traceId = res.headers.get('x-trace-id'); 61 | if (traceId === null) { 62 | expect(traceId).not.toBeNull(); 63 | return; 64 | } 65 | const trace = await getTrace(collectorWorker, traceId); 66 | 67 | const span = trace[0]; 68 | expect(span.traceId).toBe(traceId); 69 | expect(span.name).toBe('Request (fetch event)'); 70 | expect(span.localEndpoint.serviceName).toBe('zipkin-basic'); 71 | 72 | // Check attributes 73 | expect(span.tags?.[ATTRIBUTE_NAME.SERVICE_NAME]).toBe('zipkin-basic'); 74 | expect(span.tags?.[ATTRIBUTE_NAME.SDK_NAME]).toBe('workers-tracing'); 75 | expect(span.tags?.[ATTRIBUTE_NAME.SDK_LANG]).toBe('javascript'); 76 | expect(span.tags?.[ATTRIBUTE_NAME.SDK_VERSION]).toBe('__VERSION__'); 77 | expect(span.tags?.[ATTRIBUTE_NAME.RUNTIME_NAME]).toBe('Cloudflare-Workers'); 78 | }); 79 | 80 | test('You can add attributes on resource', async () => { 81 | devWorker = await startWorker('test/scripts/zipkin/resource-attributes.ts'); 82 | 83 | const res = await devWorker.fetch('http://worker/test'); 84 | 85 | expect(res.status).toBe(200); 86 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 87 | 88 | const traceId = res.headers.get('x-trace-id'); 89 | if (traceId === null) { 90 | expect(traceId).not.toBeNull(); 91 | return; 92 | } 93 | const trace = await getTrace(collectorWorker, traceId); 94 | 95 | const span = trace[0]; 96 | expect(span.traceId).toBe(traceId); 97 | expect(span.name).toBe('Request (fetch event)'); 98 | expect(span.localEndpoint.serviceName).toBe('resource-attributes'); 99 | 100 | // Check attributes 101 | expect(span.tags?.[ATTRIBUTE_NAME.SERVICE_NAME]).toBe('resource-attributes'); 102 | expect(span.tags?.[ATTRIBUTE_NAME.SDK_NAME]).toBe('workers-tracing'); 103 | expect(span.tags?.[ATTRIBUTE_NAME.SDK_LANG]).toBe('javascript'); 104 | expect(span.tags?.[ATTRIBUTE_NAME.SDK_VERSION]).toBe('__VERSION__'); 105 | 106 | // Custom attributes 107 | expect(span.tags?.['exampleAttribute']).toBe('true'); 108 | expect(span.tags?.[ATTRIBUTE_NAME.RUNTIME_NAME]).toBe('blob-runtime'); 109 | }); 110 | }); 111 | 112 | describe('Single span', () => { 113 | test('You can add a single span', async () => { 114 | devWorker = await startWorker('test/scripts/zipkin/single-span.ts'); 115 | 116 | const res = await devWorker.fetch('http://worker/test'); 117 | 118 | expect(res.status).toBe(200); 119 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 120 | 121 | const traceId = res.headers.get('x-trace-id'); 122 | if (traceId === null) { 123 | expect(traceId).not.toBeNull(); 124 | return; 125 | } 126 | const trace = await getTrace(collectorWorker, traceId); 127 | 128 | // Root + child 129 | expect(trace.length).toBe(2); 130 | 131 | // Root span 132 | const rootSpan = trace[0]; 133 | expect(rootSpan.traceId).toBe(traceId); 134 | expect(rootSpan.name).toBe('Request (fetch event)'); 135 | expect(rootSpan.localEndpoint.serviceName).toBe('single-span'); 136 | 137 | // Child span 138 | const childSpan = trace[1]; 139 | expect(childSpan.traceId).toBe(traceId); 140 | expect(childSpan.parentId).toBe(rootSpan.id); 141 | expect(childSpan.name).toBe(SPAN_NAME.FETCH); 142 | expect(childSpan.duration).not.toBe(0); 143 | }); 144 | 145 | test('You can add a single span with attributes', async () => { 146 | devWorker = await startWorker('test/scripts/zipkin/single-span-attributes.ts'); 147 | 148 | const res = await devWorker.fetch('http://worker/test'); 149 | 150 | expect(res.status).toBe(200); 151 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 152 | 153 | const traceId = res.headers.get('x-trace-id'); 154 | if (traceId === null) { 155 | expect(traceId).not.toBeNull(); 156 | return; 157 | } 158 | const trace = await getTrace(collectorWorker, traceId); 159 | 160 | // Root + child 161 | expect(trace.length).toBe(2); 162 | 163 | // Root span 164 | const rootSpan = trace[0]; 165 | expect(rootSpan.traceId).toBe(traceId); 166 | expect(rootSpan.name).toBe('Request (fetch event)'); 167 | expect(rootSpan.localEndpoint.serviceName).toBe('single-span-attributes'); 168 | 169 | // Child span 170 | const childSpan = trace[1]; 171 | expect(childSpan.traceId).toBe(traceId); 172 | expect(childSpan.parentId).toBe(rootSpan.id); 173 | expect(childSpan.name).toBe(SPAN_NAME.FETCH); 174 | expect(childSpan.duration).not.toBe(0); 175 | expect(childSpan.tags?.[ATTRIBUTE_NAME.HTTP_HOST]).toBe('example.com'); 176 | }); 177 | 178 | test('You can add a single span with events', async () => { 179 | devWorker = await startWorker('test/scripts/zipkin/single-span-events.ts'); 180 | 181 | const res = await devWorker.fetch('http://worker/test'); 182 | 183 | expect(res.status).toBe(200); 184 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 185 | 186 | const traceId = res.headers.get('x-trace-id'); 187 | if (traceId === null) { 188 | expect(traceId).not.toBeNull(); 189 | return; 190 | } 191 | const trace = await getTrace(collectorWorker, traceId); 192 | 193 | // Root + child 194 | expect(trace.length).toBe(2); 195 | 196 | // Root span 197 | const rootSpan = trace[0]; 198 | expect(rootSpan.traceId).toBe(traceId); 199 | expect(rootSpan.name).toBe('Request (fetch event)'); 200 | expect(rootSpan.localEndpoint.serviceName).toBe('single-span-events'); 201 | 202 | // Child span 203 | const childSpan = trace[1]; 204 | expect(childSpan.traceId).toBe(traceId); 205 | expect(childSpan.parentId).toBe(rootSpan.id); 206 | expect(childSpan.name).toBe(SPAN_NAME.FETCH); 207 | expect(childSpan.duration).not.toBe(0); 208 | 209 | // Annotations (events) 210 | expect(childSpan.annotations?.length).toBe(2); 211 | expect(childSpan.annotations?.[0].value).toBe('Fetch done'); 212 | expect(childSpan.annotations?.[0].timestamp).not.toBe(0); 213 | expect(childSpan.annotations?.[1].value).toBe('Response body parsed'); 214 | expect(childSpan.annotations?.[1].timestamp).not.toBe(0); 215 | }); 216 | 217 | test('You can add a single span with attributes and events', async () => { 218 | devWorker = await startWorker('test/scripts/zipkin/single-span-attributes-and-events.ts'); 219 | 220 | const res = await devWorker.fetch('http://worker/test'); 221 | 222 | expect(res.status).toBe(200); 223 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 224 | 225 | const traceId = res.headers.get('x-trace-id'); 226 | if (traceId === null) { 227 | expect(traceId).not.toBeNull(); 228 | return; 229 | } 230 | const trace = await getTrace(collectorWorker, traceId); 231 | 232 | // Root + child 233 | expect(trace.length).toBe(2); 234 | 235 | // Root span 236 | const rootSpan = trace[0]; 237 | expect(rootSpan.traceId).toBe(traceId); 238 | expect(rootSpan.name).toBe('Request (fetch event)'); 239 | expect(rootSpan.localEndpoint.serviceName).toBe('single-span-attributes-and-events'); 240 | 241 | // Child span 242 | const childSpan = trace[1]; 243 | expect(childSpan.traceId).toBe(traceId); 244 | expect(childSpan.parentId).toBe(rootSpan.id); 245 | expect(childSpan.name).toBe(SPAN_NAME.FETCH); 246 | expect(childSpan.duration).not.toBe(0); 247 | expect(childSpan.tags?.[ATTRIBUTE_NAME.HTTP_HOST]).toBe('example.com'); 248 | 249 | // Annotations (events) 250 | expect(childSpan.annotations?.length).toBe(2); 251 | expect(childSpan.annotations?.[0].value).toBe('Fetch done'); 252 | expect(childSpan.annotations?.[0].timestamp).not.toBe(0); 253 | expect(childSpan.annotations?.[1].value).toBe('Response body parsed'); 254 | expect(childSpan.annotations?.[1].timestamp).not.toBe(0); 255 | }); 256 | }); 257 | 258 | describe('Multiple spans', () => { 259 | test('You can add multiple spans', async () => { 260 | devWorker = await startWorker('test/scripts/zipkin/multiple-spans.ts', { 261 | kv: [ { binding: 'KV', id: '' } ], 262 | }); 263 | 264 | const res = await devWorker.fetch('http://worker/test'); 265 | 266 | expect(res.status).toBe(200); 267 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 268 | 269 | const traceId = res.headers.get('x-trace-id'); 270 | if (traceId === null) { 271 | expect(traceId).not.toBeNull(); 272 | return; 273 | } 274 | const trace = await getTrace(collectorWorker, traceId); 275 | 276 | // Root + 2 children 277 | expect(trace.length).toBe(3); 278 | 279 | // Root span 280 | const rootSpan = trace[0]; 281 | expect(rootSpan.traceId).toBe(traceId); 282 | expect(rootSpan.name).toBe('Request (fetch event)'); 283 | expect(rootSpan.localEndpoint.serviceName).toBe('multiple-spans'); 284 | 285 | // First child span 286 | const firstChildSpan = trace[1]; 287 | expect(firstChildSpan.traceId).toBe(traceId); 288 | expect(firstChildSpan.parentId).toBe(rootSpan.id); 289 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 290 | expect(firstChildSpan.duration).not.toBe(0); 291 | 292 | // Second child span 293 | const secondChildSpan = trace[2]; 294 | expect(secondChildSpan.traceId).toBe(traceId); 295 | expect(secondChildSpan.parentId).toBe(rootSpan.id); 296 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 297 | expect(secondChildSpan.duration).not.toBe(0); 298 | }); 299 | 300 | test('You can add multiple spans with attributes', async () => { 301 | devWorker = await startWorker('test/scripts/zipkin/multiple-spans-attributes.ts', { 302 | kv: [ { binding: 'KV', id: '' } ], 303 | }); 304 | 305 | const res = await devWorker.fetch('http://worker/test'); 306 | 307 | expect(res.status).toBe(200); 308 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 309 | 310 | const traceId = res.headers.get('x-trace-id'); 311 | if (traceId === null) { 312 | expect(traceId).not.toBeNull(); 313 | return; 314 | } 315 | const trace = await getTrace(collectorWorker, traceId); 316 | 317 | // Root + 2 children 318 | expect(trace.length).toBe(3); 319 | 320 | // Root span 321 | const rootSpan = trace[0]; 322 | expect(rootSpan.traceId).toBe(traceId); 323 | expect(rootSpan.name).toBe('Request (fetch event)'); 324 | expect(rootSpan.localEndpoint.serviceName).toBe('multiple-spans-attributes'); 325 | 326 | // First child span 327 | const firstChildSpan = trace[1]; 328 | expect(firstChildSpan.traceId).toBe(traceId); 329 | expect(firstChildSpan.parentId).toBe(rootSpan.id); 330 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 331 | expect(firstChildSpan.duration).not.toBe(0); 332 | expect(firstChildSpan.tags?.[ATTRIBUTE_NAME.HTTP_HOST]).toBe('example.com'); 333 | 334 | // Second child span 335 | const secondChildSpan = trace[2]; 336 | expect(secondChildSpan.traceId).toBe(traceId); 337 | expect(secondChildSpan.parentId).toBe(rootSpan.id); 338 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 339 | expect(secondChildSpan.duration).not.toBe(0); 340 | expect(secondChildSpan.tags?.[ATTRIBUTE_NAME.KV_KEY]).toBe('abc'); 341 | }); 342 | 343 | test('You can add multiple spans with events', async () => { 344 | devWorker = await startWorker('test/scripts/zipkin/multiple-spans-events.ts', { 345 | kv: [ { binding: 'KV', id: '' } ], 346 | }); 347 | 348 | const res = await devWorker.fetch('http://worker/test'); 349 | 350 | expect(res.status).toBe(200); 351 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 352 | 353 | const traceId = res.headers.get('x-trace-id'); 354 | if (traceId === null) { 355 | expect(traceId).not.toBeNull(); 356 | return; 357 | } 358 | const trace = await getTrace(collectorWorker, traceId); 359 | 360 | // Root + 2 children 361 | expect(trace.length).toBe(3); 362 | 363 | // Root span 364 | const rootSpan = trace[0]; 365 | expect(rootSpan.traceId).toBe(traceId); 366 | expect(rootSpan.name).toBe('Request (fetch event)'); 367 | expect(rootSpan.localEndpoint.serviceName).toBe('multiple-spans-events'); 368 | 369 | // First child span 370 | const firstChildSpan = trace[1]; 371 | expect(firstChildSpan.traceId).toBe(traceId); 372 | expect(firstChildSpan.parentId).toBe(rootSpan.id); 373 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 374 | expect(firstChildSpan.duration).not.toBe(0); 375 | 376 | // - Annotations (events) 377 | expect(firstChildSpan.annotations?.length).toBe(2); 378 | expect(firstChildSpan.annotations?.[0].value).toBe('Fetch done'); 379 | expect(firstChildSpan.annotations?.[0].timestamp).not.toBe(0); 380 | expect(firstChildSpan.annotations?.[1].value).toBe('Response body parsed'); 381 | expect(firstChildSpan.annotations?.[1].timestamp).not.toBe(0); 382 | 383 | // Second child span 384 | const secondChildSpan = trace[2]; 385 | expect(secondChildSpan.traceId).toBe(traceId); 386 | expect(secondChildSpan.parentId).toBe(rootSpan.id); 387 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 388 | expect(secondChildSpan.duration).not.toBe(0); 389 | 390 | // - Annotations (events) 391 | expect(secondChildSpan.annotations?.length).toBe(1); 392 | expect(secondChildSpan.annotations?.[0].value).toBe('KV get done'); 393 | expect(secondChildSpan.annotations?.[0].timestamp).not.toBe(0); 394 | }); 395 | 396 | test('You can add multiple spans with attributes and events', async () => { 397 | devWorker = await startWorker('test/scripts/zipkin/multiple-spans-attributes-and-events.ts', { 398 | kv: [ { binding: 'KV', id: '' } ], 399 | }); 400 | 401 | const res = await devWorker.fetch('http://worker/test'); 402 | 403 | expect(res.status).toBe(200); 404 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 405 | 406 | const traceId = res.headers.get('x-trace-id'); 407 | if (traceId === null) { 408 | expect(traceId).not.toBeNull(); 409 | return; 410 | } 411 | const trace = await getTrace(collectorWorker, traceId); 412 | 413 | // Root + 2 children 414 | expect(trace.length).toBe(3); 415 | 416 | // Root span 417 | const rootSpan = trace[0]; 418 | expect(rootSpan.traceId).toBe(traceId); 419 | expect(rootSpan.name).toBe('Request (fetch event)'); 420 | expect(rootSpan.localEndpoint.serviceName).toBe('multiple-spans-attributes-and-events'); 421 | 422 | // First child span 423 | const firstChildSpan = trace[1]; 424 | expect(firstChildSpan.traceId).toBe(traceId); 425 | expect(firstChildSpan.parentId).toBe(rootSpan.id); 426 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 427 | expect(firstChildSpan.duration).not.toBe(0); 428 | expect(firstChildSpan.tags?.[ATTRIBUTE_NAME.HTTP_HOST]).toBe('example.com'); 429 | 430 | // - Annotations (events) 431 | expect(firstChildSpan.annotations?.length).toBe(2); 432 | expect(firstChildSpan.annotations?.[0].value).toBe('Fetch done'); 433 | expect(firstChildSpan.annotations?.[0].timestamp).not.toBe(0); 434 | expect(firstChildSpan.annotations?.[1].value).toBe('Response body parsed'); 435 | expect(firstChildSpan.annotations?.[1].timestamp).not.toBe(0); 436 | 437 | // Second child span 438 | const secondChildSpan = trace[2]; 439 | expect(secondChildSpan.traceId).toBe(traceId); 440 | expect(secondChildSpan.parentId).toBe(rootSpan.id); 441 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 442 | expect(secondChildSpan.duration).not.toBe(0); 443 | expect(secondChildSpan.tags?.[ATTRIBUTE_NAME.KV_KEY]).toBe('abc'); 444 | 445 | // - Annotations (events) 446 | expect(secondChildSpan.annotations?.length).toBe(1); 447 | expect(secondChildSpan.annotations?.[0].value).toBe('KV get done'); 448 | expect(secondChildSpan.annotations?.[0].timestamp).not.toBe(0); 449 | }); 450 | }); 451 | 452 | describe('Child of child span', () => { 453 | test('You can add a child to a child span', async () => { 454 | devWorker = await startWorker('test/scripts/zipkin/span-span.ts', { 455 | kv: [ { binding: 'KV', id: '' } ], 456 | }); 457 | 458 | const res = await devWorker.fetch('http://worker/test'); 459 | 460 | expect(res.status).toBe(200); 461 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 462 | 463 | const traceId = res.headers.get('x-trace-id'); 464 | if (traceId === null) { 465 | expect(traceId).not.toBeNull(); 466 | return; 467 | } 468 | const trace = await getTrace(collectorWorker, traceId); 469 | 470 | // Root + 2 children 471 | expect(trace.length).toBe(3); 472 | 473 | // Root span 474 | const rootSpan = trace[0]; 475 | expect(rootSpan.traceId).toBe(traceId); 476 | expect(rootSpan.name).toBe('Request (fetch event)'); 477 | expect(rootSpan.localEndpoint.serviceName).toBe('span-span'); 478 | 479 | // First child span 480 | const firstChildSpan = trace[1]; 481 | expect(firstChildSpan.traceId).toBe(traceId); 482 | expect(firstChildSpan.parentId).toBe(rootSpan.id); 483 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 484 | expect(firstChildSpan.duration).not.toBe(0); 485 | 486 | // Second child span 487 | const secondChildSpan = trace[2]; 488 | expect(secondChildSpan.traceId).toBe(traceId); 489 | // Validate this is a child of the first child 490 | expect(secondChildSpan.parentId).toBe(firstChildSpan.id); 491 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 492 | expect(secondChildSpan.duration).not.toBe(0); 493 | }); 494 | 495 | test('You can add a child to a child span with attributes', async () => { 496 | devWorker = await startWorker('test/scripts/zipkin/span-span-attributes.ts', { 497 | kv: [ { binding: 'KV', id: '' } ], 498 | }); 499 | 500 | const res = await devWorker.fetch('http://worker/test'); 501 | 502 | expect(res.status).toBe(200); 503 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 504 | 505 | const traceId = res.headers.get('x-trace-id'); 506 | if (traceId === null) { 507 | expect(traceId).not.toBeNull(); 508 | return; 509 | } 510 | const trace = await getTrace(collectorWorker, traceId); 511 | 512 | // Root + 2 children 513 | expect(trace.length).toBe(3); 514 | 515 | // Root span 516 | const rootSpan = trace[0]; 517 | expect(rootSpan.traceId).toBe(traceId); 518 | expect(rootSpan.name).toBe('Request (fetch event)'); 519 | expect(rootSpan.localEndpoint.serviceName).toBe('span-span-attributes'); 520 | 521 | // First child span 522 | const firstChildSpan = trace[1]; 523 | expect(firstChildSpan.traceId).toBe(traceId); 524 | expect(firstChildSpan.parentId).toBe(rootSpan.id); 525 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 526 | expect(firstChildSpan.duration).not.toBe(0); 527 | expect(firstChildSpan.tags?.[ATTRIBUTE_NAME.HTTP_HOST]).toBe('example.com'); 528 | 529 | // Second child span 530 | const secondChildSpan = trace[2]; 531 | expect(secondChildSpan.traceId).toBe(traceId); 532 | // Validate this is a child of the first child 533 | expect(secondChildSpan.parentId).toBe(firstChildSpan.id); 534 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 535 | expect(secondChildSpan.duration).not.toBe(0); 536 | expect(secondChildSpan.tags?.[ATTRIBUTE_NAME.KV_KEY]).toBe('abc'); 537 | }); 538 | 539 | test('You can add a child to a child span with events', async () => { 540 | devWorker = await startWorker('test/scripts/zipkin/span-span-events.ts', { 541 | kv: [ { binding: 'KV', id: '' } ], 542 | }); 543 | 544 | const res = await devWorker.fetch('http://worker/test'); 545 | 546 | expect(res.status).toBe(200); 547 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 548 | 549 | const traceId = res.headers.get('x-trace-id'); 550 | if (traceId === null) { 551 | expect(traceId).not.toBeNull(); 552 | return; 553 | } 554 | const trace = await getTrace(collectorWorker, traceId); 555 | 556 | // Root + 2 children 557 | expect(trace.length).toBe(3); 558 | 559 | // Root span 560 | const rootSpan = trace[0]; 561 | expect(rootSpan.traceId).toBe(traceId); 562 | expect(rootSpan.name).toBe('Request (fetch event)'); 563 | expect(rootSpan.localEndpoint.serviceName).toBe('span-span-events'); 564 | 565 | // First child span 566 | const firstChildSpan = trace[1]; 567 | expect(firstChildSpan.traceId).toBe(traceId); 568 | expect(firstChildSpan.parentId).toBe(rootSpan.id); 569 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 570 | expect(firstChildSpan.duration).not.toBe(0); 571 | 572 | // - Annotations (events) 573 | expect(firstChildSpan.annotations?.length).toBe(2); 574 | expect(firstChildSpan.annotations?.[0].value).toBe('Fetch done'); 575 | expect(firstChildSpan.annotations?.[0].timestamp).not.toBe(0); 576 | expect(firstChildSpan.annotations?.[1].value).toBe('Response body parsed'); 577 | expect(firstChildSpan.annotations?.[1].timestamp).not.toBe(0); 578 | 579 | // Second child span 580 | const secondChildSpan = trace[2]; 581 | expect(secondChildSpan.traceId).toBe(traceId); 582 | // Validate this is a child of the first child 583 | expect(secondChildSpan.parentId).toBe(firstChildSpan.id); 584 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 585 | expect(secondChildSpan.duration).not.toBe(0); 586 | 587 | // - Annotations (events) 588 | expect(secondChildSpan.annotations?.length).toBe(1); 589 | expect(secondChildSpan.annotations?.[0].value).toBe('KV get done'); 590 | expect(secondChildSpan.annotations?.[0].timestamp).not.toBe(0); 591 | }); 592 | 593 | test('You can add a child to a child span with attributes and events', async () => { 594 | devWorker = await startWorker('test/scripts/zipkin/span-span-attributes-and-events.ts', { 595 | kv: [ { binding: 'KV', id: '' } ], 596 | }); 597 | 598 | const res = await devWorker.fetch('http://worker/test'); 599 | 600 | expect(res.status).toBe(200); 601 | expect(res.headers.get('x-trace-id')).not.toBeNull(); 602 | 603 | const traceId = res.headers.get('x-trace-id'); 604 | if (traceId === null) { 605 | expect(traceId).not.toBeNull(); 606 | return; 607 | } 608 | const trace = await getTrace(collectorWorker, traceId); 609 | 610 | // Root + 2 children 611 | expect(trace.length).toBe(3); 612 | 613 | // Root span 614 | const rootSpan = trace[0]; 615 | expect(rootSpan.traceId).toBe(traceId); 616 | expect(rootSpan.name).toBe('Request (fetch event)'); 617 | expect(rootSpan.localEndpoint.serviceName).toBe('span-span-attributes-and-events'); 618 | 619 | // First child span 620 | const firstChildSpan = trace[1]; 621 | expect(firstChildSpan.traceId).toBe(traceId); 622 | expect(firstChildSpan.parentId).toBe(rootSpan.id); 623 | expect(firstChildSpan.name).toBe(SPAN_NAME.FETCH); 624 | expect(firstChildSpan.duration).not.toBe(0); 625 | expect(firstChildSpan.tags?.[ATTRIBUTE_NAME.HTTP_HOST]).toBe('example.com'); 626 | 627 | // - Annotations (events) 628 | expect(firstChildSpan.annotations?.length).toBe(2); 629 | expect(firstChildSpan.annotations?.[0].value).toBe('Fetch done'); 630 | expect(firstChildSpan.annotations?.[0].timestamp).not.toBe(0); 631 | expect(firstChildSpan.annotations?.[1].value).toBe('Response body parsed'); 632 | expect(firstChildSpan.annotations?.[1].timestamp).not.toBe(0); 633 | 634 | // Second child span 635 | const secondChildSpan = trace[2]; 636 | expect(secondChildSpan.traceId).toBe(traceId); 637 | // Validate this is a child of the first child 638 | expect(secondChildSpan.parentId).toBe(firstChildSpan.id); 639 | expect(secondChildSpan.name).toBe(SPAN_NAME.KV_GET); 640 | expect(secondChildSpan.duration).not.toBe(0); 641 | expect(secondChildSpan.tags?.[ATTRIBUTE_NAME.KV_KEY]).toBe('abc'); 642 | 643 | // - Annotations (events) 644 | expect(secondChildSpan.annotations?.length).toBe(1); 645 | expect(secondChildSpan.annotations?.[0].value).toBe('KV get done'); 646 | expect(secondChildSpan.annotations?.[0].timestamp).not.toBe(0); 647 | }); 648 | }); 649 | }); 650 | -------------------------------------------------------------------------------- /tsconfig.emit.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "outDir": "dist", 8 | "lib": ["ES2020"], 9 | "types": ["@cloudflare/workers-types"], 10 | "baseUrl": "src", 11 | "paths": { 12 | "src/*": ["*"] 13 | } 14 | }, 15 | "include": ["src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "module": "commonjs", 5 | "esModuleInterop": true, 6 | "forceConsistentCasingInFileNames": true, 7 | "strict": true, 8 | "skipLibCheck": true, 9 | "lib": ["ES2020"], 10 | "types": ["@cloudflare/workers-types"], 11 | "baseUrl": "src", 12 | "paths": { 13 | "src/*": ["*"] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite'; 3 | 4 | export default defineConfig({ 5 | 6 | }); 7 | --------------------------------------------------------------------------------