├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .release-it.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── TRPC.md ├── aws_lambda.md ├── examples ├── fargate-express-alb │ ├── .dockerignore │ ├── .gitignore │ ├── .vscode │ │ ├── launch.json │ │ └── settings.json │ ├── Dockerfile │ ├── cdk.context.json │ ├── package-lock.json │ ├── package.json │ ├── src │ │ ├── index.js │ │ └── tracing.js │ ├── sst.config.ts │ ├── stacks │ │ └── MyStack.ts │ └── tsconfig.json ├── nextjs │ ├── .gitignore │ ├── README.md │ ├── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.module.css │ │ └── page.tsx │ ├── instrumentation.ts │ ├── next.config.mjs │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── next.svg │ │ └── vercel.svg │ └── tsconfig.json ├── serverless-otel │ ├── .gitignore │ ├── .vscode │ │ ├── launch.json │ │ └── settings.json │ ├── package-lock.json │ ├── package.json │ ├── packages │ │ └── functions │ │ │ ├── package.json │ │ │ ├── src │ │ │ ├── lambda.ts │ │ │ └── tracing.ts │ │ │ ├── sst-env.d.ts │ │ │ └── tsconfig.json │ ├── pnpm-workspace.yaml │ ├── sst.config.ts │ ├── stacks │ │ └── MyStack.ts │ └── tsconfig.json ├── sst-service │ ├── .dockerignore │ ├── .gitignore │ ├── .vscode │ │ ├── launch.json │ │ └── settings.json │ ├── Dockerfile │ ├── cdk.context.json │ ├── package-lock.json │ ├── package.json │ ├── pnpm-workspace.yaml │ ├── src │ │ ├── index.js │ │ └── tracing.cjs │ ├── sst.config.ts │ ├── stacks │ │ └── MyStack.ts │ └── tsconfig.json └── state-machine │ ├── .gitignore │ ├── .vscode │ ├── launch.json │ └── settings.json │ ├── package-lock.json │ ├── package.json │ ├── packages │ └── functions │ │ ├── package.json │ │ ├── src │ │ ├── lambda.ts │ │ ├── task-one.ts │ │ ├── task-three.ts │ │ ├── task-two.ts │ │ └── tracing.ts │ │ ├── sst-env.d.ts │ │ └── tsconfig.json │ ├── pnpm-workspace.yaml │ ├── sst.config.ts │ ├── stacks │ └── MyStack.ts │ └── tsconfig.json ├── package-lock.json ├── package.json ├── patches ├── @opentelemetry+instrumentation+0.48.0.patch └── @opentelemetry+instrumentation+0.50.0.patch ├── src ├── baselime.ts ├── http-plugins │ ├── plugin.ts │ ├── stripe.ts │ └── vercel.ts ├── http.ts ├── http │ ├── enums │ │ └── AttributeNames.ts │ ├── index.ts │ ├── readme.md │ ├── types.ts │ └── utils.ts ├── index.ts ├── lambda.ts ├── lambda │ ├── index.ts │ ├── parse-event.ts │ ├── propation.ts │ └── utils.ts ├── resources │ ├── koyeb.ts │ ├── service.ts │ └── vercel.ts ├── trpc.ts └── utils │ └── utils.ts ├── tests ├── http.test.ts ├── index.test.ts └── utils │ └── otel.ts ├── trace-2.png ├── traces.png ├── trpc.png ├── tsconfig.json ├── tslint.json └── tsup.config.ts /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | publish-node-packages: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | packages: write 14 | id-token: write 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v2 19 | 20 | - name: Setup 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: '18.x' 24 | registry-url: 'https://registry.npmjs.org' 25 | 26 | - name: Build and publish to npm registry 27 | continue-on-error: true 28 | run: 29 | npm ci && 30 | npx patch-package && 31 | npm run build && 32 | npm run check && 33 | npm publish --access public 34 | env: 35 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | - uses: actions/setup-node@v2 37 | with: 38 | node-version: '16.x' 39 | registry-url: 'https://npm.pkg.github.com' 40 | scope: "@baselime" 41 | 42 | - run: echo "registry=https://npm.pkg.github.com/@baselime" >> .npmrc 43 | 44 | - name: Publish to GitHub registry 45 | continue-on-error: true 46 | run: npm publish 47 | env: 48 | NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | create-release: 51 | needs: [publish-node-packages] 52 | 53 | runs-on: ubuntu-latest 54 | 55 | steps: 56 | - name: Checkout 57 | uses: actions/checkout@v3 58 | 59 | - name: Set version 60 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV 61 | 62 | - name: Set current date 63 | run: | 64 | echo "RELEASE_DATE=$(date +"%d %B %Y")" >> $GITHUB_ENV 65 | 66 | - name: Get version from tag 67 | id: tag_name 68 | run: | 69 | echo ::set-output name=current_version::${GITHUB_REF#refs/tags/v} 70 | 71 | - name: Get Changelog Entry 72 | id: changelog_reader 73 | uses: mindsers/changelog-reader-action@v2 74 | with: 75 | validation_level: none 76 | version: ${{ steps.tag_name.outputs.current_version }} 77 | path: ./CHANGELOG.md 78 | 79 | - name: Compute checksums 80 | run: | 81 | echo "## ${{ env.RELEASE_VERSION }} (${{ env.RELEASE_DATE }})" >> checksums.md 82 | echo "${{ steps.changelog_reader.outputs.changes }}" >> checksums.md 83 | echo "" >> checksums.md 84 | echo "" >> checksums.md 85 | 86 | - name: Release 87 | uses: softprops/action-gh-release@v1 88 | with: 89 | prerelease: false 90 | body_path: checksums.md 91 | files: | 92 | LICENSE 93 | env: 94 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 95 | 96 | notify-community: 97 | needs: [create-release] 98 | runs-on: ubuntu-latest 99 | steps: 100 | - name: Checkout 101 | uses: actions/checkout@v2 102 | - name: Get version from tag 103 | id: tag_name 104 | run: | 105 | echo ::set-output name=current_version::${GITHUB_REF#refs/tags/v} 106 | - name: Post to the community Slack channel 107 | uses: slackapi/slack-github-action@v1.23.0 108 | with: 109 | channel-id: 'C04KT9JNRHS' 110 | payload: | 111 | { 112 | "text": "[Release] Baselime Opentelemetry for Node.JS v${{ steps.tag_name.outputs.current_version }}", 113 | "blocks": [ 114 | { 115 | "type": "section", 116 | "text": { 117 | "type": "mrkdwn", 118 | "text": "*[Release] Baselime Opentelemetry for Node.JS v${{ steps.tag_name.outputs.current_version }}*" 119 | } 120 | }, 121 | { 122 | "type": "section", 123 | "text": { 124 | "type": "mrkdwn", 125 | "text": "" 126 | } 127 | } 128 | ] 129 | } 130 | env: 131 | SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "Test" 2 | on: 3 | push: 4 | branches: 5 | - "**" 6 | jobs: 7 | test: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: '18' 14 | - name: Install dependencies 15 | run: npm ci 16 | - name: Test 17 | run: npm run test 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # Docusaurus cache and generated files 108 | .docusaurus 109 | 110 | # Serverless directories 111 | .serverless/ 112 | 113 | # FuseBox cache 114 | .fusebox/ 115 | 116 | # DynamoDB Local files 117 | .dynamodb/ 118 | 119 | # TernJS port file 120 | .tern-port 121 | 122 | # Stores VSCode versions used for testing VSCode extensions 123 | .vscode-test 124 | 125 | # yarn v2 126 | .yarn/cache 127 | .yarn/unplugged 128 | .yarn/build-state.yml 129 | .yarn/install-state.gz 130 | .pnp.* 131 | 132 | index.cjs 133 | index.js 134 | index.d.ts 135 | index.d.cts 136 | 137 | trpc.cjs 138 | trpc.js 139 | trpc.d.ts 140 | trpc.d.cts 141 | -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true 4 | }, 5 | "git": { 6 | "tagName": "v${version}" 7 | }, 8 | "npm": { 9 | "publish": false 10 | }, 11 | "plugins": { 12 | "@release-it/keep-a-changelog": { 13 | "filename": "CHANGELOG.md" 14 | } 15 | } 16 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | 6 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 7 | 8 | ## [0.5.8] - 2024-04-04 9 | 10 | * fix types for vercel plugin 11 | 12 | ## [0.5.7] - 2024-04-04 13 | 14 | * Upgrade the sdk 15 | 16 | ## [0.5.6] - 2024-04-04 17 | 18 | * Manual Tracing for lambda. 19 | * Configurable withOpenTelemetry Hook for Lambda 20 | 21 | ## [0.5.5] - 2024-03-28 22 | 23 | * Pass through errors correctly 24 | 25 | ## [0.5.4] - 2024-03-28 26 | 27 | * handle modified traceparent better 28 | 29 | ## [0.5.3] - 2024-03-28 30 | 31 | * Handle undefined traceparent for step function propagation 32 | 33 | ## [0.5.2] - 2024-03-28 34 | 35 | * make tracing key _baselime for step-functions 36 | 37 | ## [0.5.1] - 2024-03-28 38 | 39 | * Improve step function tracing 40 | 41 | ## [0.5.0] - 2024-03-27 42 | 43 | * Refactor Lambda Handling 44 | * Step function support 45 | 46 | ## [0.4.8] - 2024-03-18 47 | 48 | * add support for custom attributes 49 | 50 | ## [0.4.7] - 2024-03-18 51 | 52 | * add support for passing custom detectors 53 | 54 | ## [0.4.6] - 2024-03-11 55 | 56 | - fix package.json 57 | 58 | ## [0.4.5] - 2024-03-11 59 | 60 | * remove esm build 61 | 62 | ## [0.4.4] - 2024-03-11 63 | 64 | * fix build for next.js projects 65 | 66 | ## [0.4.3] - 2024-03-04 67 | 68 | * Handle lambda timeouts gracefully 69 | 70 | ## [0.4.2] - 2024-03-04 71 | 72 | * fix: install script 73 | 74 | ## [0.4.1] - 2024-03-04 75 | 76 | * Bundle seperately and package opentelemetry 77 | * Fix import-in-the-middle import with a patch 78 | 79 | ## [0.4.0] - 2024-03-02 80 | 81 | * Support for capturing http client request and response bodies 82 | 83 | 84 | ## [0.3.9] - 2024-02-29 85 | 86 | * adds some utils for aws lambda 87 | 88 | ## [0.3.8] - 2024-02-15 89 | 90 | * fix trpc packaging 91 | 92 | ## [0.3.7] - 2024-02-14 93 | 94 | * remove duplicate setup 95 | 96 | ## [0.3.6] - 2024-02-12 97 | 98 | * adjust log options to reduce noise 99 | 100 | ## [0.3.5] - 2024-02-12 101 | 102 | * update package lock to update trpc/server 103 | 104 | ## [0.3.4] - 2024-02-12 105 | 106 | * fix ts error in build 107 | 108 | ## [0.3.3] - 2024-02-12 109 | 110 | * support trpc v11 111 | 112 | ## [0.3.2] - 2024-02-12 113 | 114 | * allow optional sampling 115 | 116 | ## [0.3.1] - 2024-02-12 117 | 118 | * Log to console if no api key present 119 | 120 | ## [0.3.0] - 2023-12-14 121 | 122 | - fix overriding service with env 123 | - Koyeb Service Discovery 124 | 125 | 126 | ## [0.2.14] - 2023-11-20 127 | 128 | - upgrade packages 129 | 130 | ## [0.2.13] - 2023-11-17 131 | 132 | - pass through service name 133 | 134 | ## [0.2.12] - 2023-11-14 135 | 136 | - debug and timeout in 1 second 137 | 138 | ## [0.2.11] - 2023-11-07 139 | 140 | - publish to github packages 141 | 142 | ## [0.2.10] - 2023-11-04 143 | 144 | - api key fallback 145 | 146 | ## [0.2.9] - 2023-11-04 147 | 148 | - Add Request Body Parsing 149 | - Add API KEY Tracer disabling 150 | 151 | ## [0.2.8] - 2023-10-19 152 | 153 | - Improve service and namespace detection for vercel 154 | - Document TRPC middleware 155 | 156 | ## [0.2.7] - 2023-10-19 157 | 158 | - Better Plugin api 159 | 160 | ## [0.2.6] - 2023-10-18 161 | 162 | - http plugin format 163 | 164 | ## [0.2.5] - 2023-10-18 165 | 166 | - fix npm package exports for cjs 167 | 168 | ## [0.2.4] - 2023-10-18 169 | 170 | - bundle correctly 171 | 172 | ## [0.2.3] - 2023-10-17 173 | 174 | - fix 175 | 176 | ## [0.2.2] - 2023-10-17 177 | 178 | - Vercel Env Detection 179 | 180 | ## [0.2.2-0] - 2023-10-17 181 | - trpc 182 | 183 | ## [0.2.1] - 2023-10-15 184 | 185 | - cjs exports 186 | 187 | ## [0.2.0] - 2023-10-15 188 | 189 | - optimised build 2.8mb -> 175kb 190 | - fixed resource attributes 191 | - serverless mode - use simplespanprocessor to not drop spans in lambda 192 | - now this library is tested 193 | - added support for telemetry extensions 194 | 195 | ## [0.1.0] 2023-08-10 196 | 197 | - initial version 198 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {2023} {Baselime Limited} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Node.js Baselime OpenTelemetry SDK 2 | [![Documentation][docs_badge]][docs] 3 | [![Latest Release][release_badge]][release] 4 | [![License][license_badge]][license] 5 | 6 | Instrument your Node.js applications with OpenTelemetry and send the traces to [Baselime](https://baselime.io). 7 | 8 | ![A baselime trace diagram](./traces.png) 9 | 10 | 11 | ## Getting Started 12 | 13 | Check out the [documentation](https://baselime.io/docs/sending-data/opentelemetry/). 14 | 15 | ## Example 16 | 17 | ```javascript 18 | import { BaselimeSDK } from '@baselime/node-opentelemetry'; 19 | import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; 20 | 21 | 22 | const sdk = new BaselimeSDK({ 23 | instrumentations: [ 24 | getNodeAutoInstrumentations(), 25 | ], 26 | }); 27 | 28 | sdk.start(); 29 | ``` 30 | 31 | ## Configuration 32 | 33 | The BaselimeSDK class takes the following configuration options 34 | 35 | | Field | Type | Description | 36 | | ---------------- | ----------------------- | ------------------------------------ | 37 | | instrumentations | InstrumentationOption[] | An array of instrumentation options. | 38 | | baselimeKey | string (optional) | The Baselime key. | 39 | | collectorUrl | string (optional) | The URL of the collector. | 40 | | service | string (optional) | The service name. | 41 | | namespace | string (optional) | The namespace. | 42 | 43 | ## License 44 | 45 | © Baselime Limited, 2023 46 | 47 | Distributed under Apache 2 License (`Apache-2.0`). 48 | 49 | See [LICENSE](LICENSE) for more information. 50 | 51 | 52 | 53 | [docs]: https://baselime.io/docs/ 54 | [docs_badge]: https://img.shields.io/badge/docs-reference-blue.svg?style=flat-square 55 | [release]: https://github.com/baselime/node-opentelemetry/releases/latest 56 | [release_badge]: https://img.shields.io/github/release/baselime/node-opentelemetry.svg?style=flat-square&ghcache=unused 57 | [license]: https://opensource.org/licenses/MIT 58 | [license_badge]: https://img.shields.io/github/license/baselime/node-opentelemetry.svg?color=blue&style=flat-square&ghcache=unused 59 | -------------------------------------------------------------------------------- /TRPC.md: -------------------------------------------------------------------------------- 1 | # TRPC OpenTelemetry Middleware 2 | 3 | Trace your TRPC Applications. If you don't use OpenTelemetry yet please see [The Baselime Docs](https://baselime.io/docs/sending-data/opentelemetry) 4 | 5 | ![Trace TRPC](trpc.png) 6 | 7 | ## Installation 8 | 9 | ```bash 10 | npm i @baselime/node-opentelemetry 11 | ``` 12 | 13 | ## Setup 14 | 15 | Add the Middleware to the procedures you want to trace 16 | 17 | ```typescript 18 | // trpc.ts 19 | import { tracing } from "@baselime/node-opentelemetry/trpc"; 20 | 21 | const t = initTRPC.context().create({ 22 | ... 23 | }); 24 | 25 | // add the middleware to all the procedures you want to trace 26 | export const publicProcedure = t.procedure.use(trpcTracingMiddleware({ collectInput: true })) 27 | ``` 28 | -------------------------------------------------------------------------------- /aws_lambda.md: -------------------------------------------------------------------------------- 1 | # Tracing AWS Lambda Functions 2 | 3 | Manually Instrument your AWS Lambda Functions using `withOpenTelemetry` wrapper. 4 | 5 | If you want to instrument your lambda function with no code changes please read the docs [here](https://baselime.io/docs/sending-data/platforms/aws/aws-lambda/traces/node.js/). 6 | 7 | ## Getting Started 8 | 9 | Install the [`@baselime/node-opentelemetry`](https://www.npmjs.com/package/@baselime/node-opentelemetry) package. 10 | 11 | ```bash 12 | npm i @baselime/node-opentelemetry 13 | ``` 14 | 15 | ## Instrument the handler 16 | 17 | Add the following code to your lambda handler to instrument your lambda handler with [OpenTelemetry](https://opentelemetry.io/). 18 | 19 | ```javascript 20 | import { withOpenTelemetry } from "@baselime/node-opentelemetry/lambda"; 21 | 22 | export const handler = withOpenTelemetry(async () => { 23 | 24 | return { 25 | statusCode: 200, 26 | body: JSON.stringify({ 27 | message: 'Hello from Lambda!', 28 | }) 29 | }; 30 | }); 31 | ``` 32 | 33 | ## Add the Baselime OpenTelemetry SDK 34 | 35 | There are 2 ways to add the Baselime SDK to your lambda function. 36 | 37 | ### Tracing tag 38 | 39 | To automatically add the Baselime SDK and Baselime zero latency lambda extension add the `baselime:tracing` tag with the value `manual` 40 | 41 | This will enable the Baselime SDK and add the `BASELIME_API_KEY` environment variable to your lambda 42 | ### Import and Start the Baselime SDK 43 | 44 | Create a file called tracing.js and import it at the top of your lambda function 45 | 46 | ```javascript 47 | // tracing.js 48 | 49 | import { BaselimeSDK, BetterHttpInstrumentation } from '@baselime/node-opentelemetry' 50 | import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk'; 51 | 52 | 53 | new BaselimeSDK({ 54 | instrumentation: [ 55 | new AwsInstrumentation(), 56 | new BetterHttpInstrumentation({}) 57 | ] 58 | }).start(); 59 | ``` 60 | import `tracing.js` at the top of your lambda entry file" 61 | 62 | ```javascript 63 | import './tracing.js' 64 | 65 | import { withOpenTelemetry } from "@baselime/node-opentelemetry/lambda"; 66 | 67 | export const handler = withOpenTelemetry(async () => { 68 | 69 | return { 70 | statusCode: 200, 71 | body: JSON.stringify({ 72 | message: 'Hello from Lambda!', 73 | }) 74 | }; 75 | }); 76 | ``` 77 | 78 | Finally add the `BASELIME_API_KEY` environment variable to send traces to your [baselime.io](https://baselime.io) account. 79 | 80 | ## Configuration Options 81 | 82 | The `withOpenTelemetry` takes a second argument, an object with the following properties. 83 | 84 | ```javascript 85 | export const handler = withOpenTelemetry(async (e: APIGatewayProxyEventV2) => { 86 | 87 | return { 88 | statusCode: 200, 89 | body: JSON.stringify({ 90 | message: 'Hello from Lambda!', 91 | }) 92 | }; 93 | }, { 94 | captureEvent: false, 95 | captureResponse: false, 96 | proactiveInitializationThreshold: 1000, 97 | timeoutThreshold: 500, 98 | extractContext(service, event) { 99 | console.log('Extracting context', service, event); 100 | } 101 | }); 102 | ``` 103 | 104 | | Field | Type | Description | 105 | |------------------------------|---------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------| 106 | | proactiveInitializationThreshold | `number \| undefined` | Represents the threshold for proactive initialization, if provided. | 107 | | captureEvent | `boolean \| undefined` | Indicates whether to capture events or not, if specified. | 108 | | captureResponse | `boolean \| undefined` | Indicates whether to capture responses or not, if specified. | 109 | | timeoutThreshold | `number \| undefined` | Represents the timeout threshold, if specified. | 110 | | extractContext | `(service: string, event: any) => { parent?: OtelContext, links?: Link[] } \| void \| undefined` | A function that extracts context based on provided service and event, 111 | -------------------------------------------------------------------------------- /examples/fargate-express-alb/.dockerignore: -------------------------------------------------------------------------------- 1 | .sst 2 | node_modules -------------------------------------------------------------------------------- /examples/fargate-express-alb/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # sst 5 | .sst 6 | .build 7 | 8 | # opennext 9 | .open-next 10 | 11 | # misc 12 | .DS_Store 13 | 14 | # local env files 15 | .env*.local 16 | -------------------------------------------------------------------------------- /examples/fargate-express-alb/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug SST Start", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", 9 | "runtimeArgs": ["start", "--increase-timeout"], 10 | "console": "integratedTerminal", 11 | "skipFiles": ["/**"], 12 | "env": {} 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/fargate-express-alb/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.sst": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/fargate-express-alb/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | 3 | # Set the working directory inside the container 4 | WORKDIR /usr/src/app 5 | 6 | # Copy package.json and package-lock.json to the working directory 7 | COPY package*.json ./ 8 | 9 | # Install app dependencies 10 | RUN npm install --ci --omit=dev 11 | 12 | # Copy the rest of the application code to the working directory 13 | COPY src src 14 | 15 | # Expose the port your app will listen on 16 | EXPOSE 80 17 | 18 | # Command to run your app when the container starts 19 | CMD ["node","src/index.js" ] 20 | -------------------------------------------------------------------------------- /examples/fargate-express-alb/cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "availability-zones:account=522104763258:region=eu-west-1": [ 3 | "eu-west-1a", 4 | "eu-west-1b", 5 | "eu-west-1c" 6 | ], 7 | "availability-zones:account=522104763258:region=eu-west-2": [ 8 | "eu-west-2a", 9 | "eu-west-2b", 10 | "eu-west-2c" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /examples/fargate-express-alb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fargate-express-sst", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "node src/index.js", 7 | "build": "sst build", 8 | "deploy": "sst deploy", 9 | "remove": "sst remove", 10 | "console": "sst console", 11 | "typecheck": "tsc --noEmit" 12 | }, 13 | "devDependencies": { 14 | "@tsconfig/node16": "^16.1.0", 15 | "aws-cdk-lib": "2.84.0", 16 | "constructs": "10.1.156", 17 | "sst": "^2.23.15", 18 | "typescript": "^5.1.6" 19 | }, 20 | "dependencies": { 21 | "@baselime/node-opentelemetry": "^0.1.0", 22 | "@opentelemetry/auto-instrumentations-node": "^0.38.0", 23 | "express": "^4.18.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/fargate-express-alb/src/index.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const app = express(); 3 | 4 | const port = process.env.PORT || 3000; 5 | 6 | app.get("/", function (req, res) { 7 | res.send("Hello World!"); 8 | }); 9 | 10 | app.listen(port, function () { 11 | console.log(`Example app listening on port ${port}!`); 12 | }); 13 | -------------------------------------------------------------------------------- /examples/fargate-express-alb/src/tracing.js: -------------------------------------------------------------------------------- 1 | const { BaselimeSDK } = require('@baselime/node-opentelemetry'); 2 | const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); 3 | 4 | 5 | const sdk = new BaselimeSDK({ 6 | instrumentations: [ 7 | getNodeAutoInstrumentations({ 8 | '@opentelemetry/instrumentation-http': { 9 | ignoreIncomingRequestHook(request) { 10 | if(request.headers['user-agent']?.includes('HealthChecker')) { 11 | return true 12 | } 13 | return false 14 | } 15 | } 16 | }), 17 | ], 18 | }); 19 | 20 | sdk.start(); -------------------------------------------------------------------------------- /examples/fargate-express-alb/sst.config.ts: -------------------------------------------------------------------------------- 1 | import { SSTConfig } from "sst"; 2 | import { Container } from "./stacks/MyStack"; 3 | 4 | export default { 5 | config(_input) { 6 | return { 7 | name: "fargate-express-alb", 8 | region: "eu-west-2", 9 | }; 10 | }, 11 | stacks(app) { 12 | app.stack(Container); 13 | } 14 | } satisfies SSTConfig; 15 | -------------------------------------------------------------------------------- /examples/fargate-express-alb/stacks/MyStack.ts: -------------------------------------------------------------------------------- 1 | import { StackContext } from "sst/constructs"; 2 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 3 | import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; 4 | import * as ecsPatterns from 'aws-cdk-lib/aws-ecs-patterns'; 5 | import { StringParameter } from 'aws-cdk-lib/aws-ssm' 6 | import { DockerImageAsset } from "aws-cdk-lib/aws-ecr-assets"; 7 | import { Duration } from "aws-cdk-lib/core"; 8 | 9 | 10 | export function Container({ stack }: StackContext) { 11 | // Create an ECS Fargate cluster 12 | const cluster = new ecs.Cluster(stack, 'MyCluster', {}); 13 | 14 | // Build and push Docker image to ECR 15 | const asset = new DockerImageAsset(stack, "image", { 16 | directory: './', 17 | }); 18 | 19 | const loadBalancedFargateService = new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'Service', { 20 | cluster, 21 | memoryLimitMiB: 512, 22 | desiredCount: 1, 23 | cpu: 256, 24 | taskImageOptions: { 25 | image: ecs.ContainerImage.fromDockerImageAsset(asset), 26 | enableLogging: true, 27 | environment: { 28 | BASELIME_KEY: StringParameter.valueForStringParameter(stack, 'baselime-key'), 29 | PORT: '80', 30 | NODE_OPTIONS: '-r ./src/tracing.cjs', 31 | } 32 | }, 33 | publicLoadBalancer: true, 34 | targetProtocol: elbv2.ApplicationProtocol.HTTP, 35 | loadBalancerName: 'fargate-express-sst', 36 | maxHealthyPercent: 200, 37 | minHealthyPercent: 100, 38 | }); 39 | 40 | 41 | loadBalancedFargateService.targetGroup.configureHealthCheck({ 42 | healthyThresholdCount: 2, 43 | interval: Duration.seconds(5), 44 | timeout: Duration.seconds(2), 45 | }); 46 | 47 | } 48 | -------------------------------------------------------------------------------- /examples/fargate-express-alb/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "compilerOptions": { 4 | "moduleResolution": "node" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/nextjs/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /examples/nextjs/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /examples/nextjs/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baselime/node-opentelemetry/1a4b1722e0cae69c23618350b35ecd7a5e421331/examples/nextjs/app/favicon.ico -------------------------------------------------------------------------------- /examples/nextjs/app/globals.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --max-width: 1100px; 3 | --border-radius: 12px; 4 | --font-mono: ui-monospace, Menlo, Monaco, "Cascadia Mono", "Segoe UI Mono", 5 | "Roboto Mono", "Oxygen Mono", "Ubuntu Monospace", "Source Code Pro", 6 | "Fira Mono", "Droid Sans Mono", "Courier New", monospace; 7 | 8 | --foreground-rgb: 0, 0, 0; 9 | --background-start-rgb: 214, 219, 220; 10 | --background-end-rgb: 255, 255, 255; 11 | 12 | --primary-glow: conic-gradient( 13 | from 180deg at 50% 50%, 14 | #16abff33 0deg, 15 | #0885ff33 55deg, 16 | #54d6ff33 120deg, 17 | #0071ff33 160deg, 18 | transparent 360deg 19 | ); 20 | --secondary-glow: radial-gradient( 21 | rgba(255, 255, 255, 1), 22 | rgba(255, 255, 255, 0) 23 | ); 24 | 25 | --tile-start-rgb: 239, 245, 249; 26 | --tile-end-rgb: 228, 232, 233; 27 | --tile-border: conic-gradient( 28 | #00000080, 29 | #00000040, 30 | #00000030, 31 | #00000020, 32 | #00000010, 33 | #00000010, 34 | #00000080 35 | ); 36 | 37 | --callout-rgb: 238, 240, 241; 38 | --callout-border-rgb: 172, 175, 176; 39 | --card-rgb: 180, 185, 188; 40 | --card-border-rgb: 131, 134, 135; 41 | } 42 | 43 | @media (prefers-color-scheme: dark) { 44 | :root { 45 | --foreground-rgb: 255, 255, 255; 46 | --background-start-rgb: 0, 0, 0; 47 | --background-end-rgb: 0, 0, 0; 48 | 49 | --primary-glow: radial-gradient(rgba(1, 65, 255, 0.4), rgba(1, 65, 255, 0)); 50 | --secondary-glow: linear-gradient( 51 | to bottom right, 52 | rgba(1, 65, 255, 0), 53 | rgba(1, 65, 255, 0), 54 | rgba(1, 65, 255, 0.3) 55 | ); 56 | 57 | --tile-start-rgb: 2, 13, 46; 58 | --tile-end-rgb: 2, 5, 19; 59 | --tile-border: conic-gradient( 60 | #ffffff80, 61 | #ffffff40, 62 | #ffffff30, 63 | #ffffff20, 64 | #ffffff10, 65 | #ffffff10, 66 | #ffffff80 67 | ); 68 | 69 | --callout-rgb: 20, 20, 20; 70 | --callout-border-rgb: 108, 108, 108; 71 | --card-rgb: 100, 100, 100; 72 | --card-border-rgb: 200, 200, 200; 73 | } 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | padding: 0; 79 | margin: 0; 80 | } 81 | 82 | html, 83 | body { 84 | max-width: 100vw; 85 | overflow-x: hidden; 86 | } 87 | 88 | body { 89 | color: rgb(var(--foreground-rgb)); 90 | background: linear-gradient( 91 | to bottom, 92 | transparent, 93 | rgb(var(--background-end-rgb)) 94 | ) 95 | rgb(var(--background-start-rgb)); 96 | } 97 | 98 | a { 99 | color: inherit; 100 | text-decoration: none; 101 | } 102 | 103 | @media (prefers-color-scheme: dark) { 104 | html { 105 | color-scheme: dark; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /examples/nextjs/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export const metadata: Metadata = { 8 | title: "Create Next App", 9 | description: "Generated by create next app", 10 | }; 11 | 12 | export default function RootLayout({ 13 | children, 14 | }: Readonly<{ 15 | children: React.ReactNode; 16 | }>) { 17 | return ( 18 | 19 | {children} 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /examples/nextjs/app/page.module.css: -------------------------------------------------------------------------------- 1 | .main { 2 | display: flex; 3 | flex-direction: column; 4 | justify-content: space-between; 5 | align-items: center; 6 | padding: 6rem; 7 | min-height: 100vh; 8 | } 9 | 10 | .description { 11 | display: inherit; 12 | justify-content: inherit; 13 | align-items: inherit; 14 | font-size: 0.85rem; 15 | max-width: var(--max-width); 16 | width: 100%; 17 | z-index: 2; 18 | font-family: var(--font-mono); 19 | } 20 | 21 | .description a { 22 | display: flex; 23 | justify-content: center; 24 | align-items: center; 25 | gap: 0.5rem; 26 | } 27 | 28 | .description p { 29 | position: relative; 30 | margin: 0; 31 | padding: 1rem; 32 | background-color: rgba(var(--callout-rgb), 0.5); 33 | border: 1px solid rgba(var(--callout-border-rgb), 0.3); 34 | border-radius: var(--border-radius); 35 | } 36 | 37 | .code { 38 | font-weight: 700; 39 | font-family: var(--font-mono); 40 | } 41 | 42 | .grid { 43 | display: grid; 44 | grid-template-columns: repeat(4, minmax(25%, auto)); 45 | max-width: 100%; 46 | width: var(--max-width); 47 | } 48 | 49 | .card { 50 | padding: 1rem 1.2rem; 51 | border-radius: var(--border-radius); 52 | background: rgba(var(--card-rgb), 0); 53 | border: 1px solid rgba(var(--card-border-rgb), 0); 54 | transition: background 200ms, border 200ms; 55 | } 56 | 57 | .card span { 58 | display: inline-block; 59 | transition: transform 200ms; 60 | } 61 | 62 | .card h2 { 63 | font-weight: 600; 64 | margin-bottom: 0.7rem; 65 | } 66 | 67 | .card p { 68 | margin: 0; 69 | opacity: 0.6; 70 | font-size: 0.9rem; 71 | line-height: 1.5; 72 | max-width: 30ch; 73 | text-wrap: balance; 74 | } 75 | 76 | .center { 77 | display: flex; 78 | justify-content: center; 79 | align-items: center; 80 | position: relative; 81 | padding: 4rem 0; 82 | } 83 | 84 | .center::before { 85 | background: var(--secondary-glow); 86 | border-radius: 50%; 87 | width: 480px; 88 | height: 360px; 89 | margin-left: -400px; 90 | } 91 | 92 | .center::after { 93 | background: var(--primary-glow); 94 | width: 240px; 95 | height: 180px; 96 | z-index: -1; 97 | } 98 | 99 | .center::before, 100 | .center::after { 101 | content: ""; 102 | left: 50%; 103 | position: absolute; 104 | filter: blur(45px); 105 | transform: translateZ(0); 106 | } 107 | 108 | .logo { 109 | position: relative; 110 | } 111 | /* Enable hover only on non-touch devices */ 112 | @media (hover: hover) and (pointer: fine) { 113 | .card:hover { 114 | background: rgba(var(--card-rgb), 0.1); 115 | border: 1px solid rgba(var(--card-border-rgb), 0.15); 116 | } 117 | 118 | .card:hover span { 119 | transform: translateX(4px); 120 | } 121 | } 122 | 123 | @media (prefers-reduced-motion) { 124 | .card:hover span { 125 | transform: none; 126 | } 127 | } 128 | 129 | /* Mobile */ 130 | @media (max-width: 700px) { 131 | .content { 132 | padding: 4rem; 133 | } 134 | 135 | .grid { 136 | grid-template-columns: 1fr; 137 | margin-bottom: 120px; 138 | max-width: 320px; 139 | text-align: center; 140 | } 141 | 142 | .card { 143 | padding: 1rem 2.5rem; 144 | } 145 | 146 | .card h2 { 147 | margin-bottom: 0.5rem; 148 | } 149 | 150 | .center { 151 | padding: 8rem 0 6rem; 152 | } 153 | 154 | .center::before { 155 | transform: none; 156 | height: 300px; 157 | } 158 | 159 | .description { 160 | font-size: 0.8rem; 161 | } 162 | 163 | .description a { 164 | padding: 1rem; 165 | } 166 | 167 | .description p, 168 | .description div { 169 | display: flex; 170 | justify-content: center; 171 | position: fixed; 172 | width: 100%; 173 | } 174 | 175 | .description p { 176 | align-items: center; 177 | inset: 0 0 auto; 178 | padding: 2rem 1rem 1.4rem; 179 | border-radius: 0; 180 | border: none; 181 | border-bottom: 1px solid rgba(var(--callout-border-rgb), 0.25); 182 | background: linear-gradient( 183 | to bottom, 184 | rgba(var(--background-start-rgb), 1), 185 | rgba(var(--callout-rgb), 0.5) 186 | ); 187 | background-clip: padding-box; 188 | backdrop-filter: blur(24px); 189 | } 190 | 191 | .description div { 192 | align-items: flex-end; 193 | pointer-events: none; 194 | inset: auto 0 0; 195 | padding: 2rem; 196 | height: 200px; 197 | background: linear-gradient( 198 | to bottom, 199 | transparent 0%, 200 | rgb(var(--background-end-rgb)) 40% 201 | ); 202 | z-index: 1; 203 | } 204 | } 205 | 206 | /* Tablet and Smaller Desktop */ 207 | @media (min-width: 701px) and (max-width: 1120px) { 208 | .grid { 209 | grid-template-columns: repeat(2, 50%); 210 | } 211 | } 212 | 213 | @media (prefers-color-scheme: dark) { 214 | .vercelLogo { 215 | filter: invert(1); 216 | } 217 | 218 | .logo { 219 | filter: invert(1) drop-shadow(0 0 0.3rem #ffffff70); 220 | } 221 | } 222 | 223 | @keyframes rotate { 224 | from { 225 | transform: rotate(360deg); 226 | } 227 | to { 228 | transform: rotate(0deg); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /examples/nextjs/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image"; 2 | import styles from "./page.module.css"; 3 | 4 | export default function Home() { 5 | return ( 6 |
7 |
8 |

9 | Get started by editing  10 | app/page.tsx 11 |

12 | 29 |
30 | 31 |
32 | Next.js Logo 40 |
41 | 42 |
43 | 49 |

50 | Docs -> 51 |

52 |

Find in-depth information about Next.js features and API.

53 |
54 | 55 | 61 |

62 | Learn -> 63 |

64 |

Learn about Next.js in an interactive course with quizzes!

65 |
66 | 67 | 73 |

74 | Templates -> 75 |

76 |

Explore starter templates for Next.js.

77 |
78 | 79 | 85 |

86 | Deploy -> 87 |

88 |

89 | Instantly deploy your Next.js site to a shareable URL with Vercel. 90 |

91 |
92 |
93 |
94 | ); 95 | } 96 | -------------------------------------------------------------------------------- /examples/nextjs/instrumentation.ts: -------------------------------------------------------------------------------- 1 | export async function register() { 2 | if (process.env.NEXT_RUNTIME === 'nodejs') { 3 | const { BaselimeSDK, VercelPlugin, BetterHttpInstrumentation } = await import('../../dist/index.cjs'); 4 | const sdk = new BaselimeSDK({ 5 | serverless: true, 6 | service: "your-project-name", 7 | collectorUrl: "https://otel.baselime.cc/v1", 8 | instrumentations: [ 9 | // new HttpInstrumentation(), 10 | new BetterHttpInstrumentation({ 11 | plugins: [ 12 | // Add the Vercel plugin to enable correlation between your logs and traces for projects deployed on Vercel 13 | new VercelPlugin() 14 | ] 15 | }), 16 | ] 17 | }); 18 | 19 | sdk.start(); 20 | } 21 | } -------------------------------------------------------------------------------- /examples/nextjs/next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | instrumentationHook: true, 5 | } 6 | }; 7 | 8 | export default nextConfig; 9 | -------------------------------------------------------------------------------- /examples/nextjs/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs", 3 | "version": "0.1.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "nextjs", 9 | "version": "0.1.0", 10 | "dependencies": { 11 | "next": "14.0.0", 12 | "react": "^18", 13 | "react-dom": "^18" 14 | }, 15 | "devDependencies": { 16 | "@types/node": "^20", 17 | "@types/react": "^18", 18 | "@types/react-dom": "^18", 19 | "typescript": "^5" 20 | } 21 | }, 22 | "node_modules/@next/env": { 23 | "version": "14.0.0", 24 | "resolved": "https://registry.npmjs.org/@next/env/-/env-14.0.0.tgz", 25 | "integrity": "sha512-cIKhxkfVELB6hFjYsbtEeTus2mwrTC+JissfZYM0n+8Fv+g8ucUfOlm3VEDtwtwydZ0Nuauv3bl0qF82nnCAqA==" 26 | }, 27 | "node_modules/@next/swc-darwin-arm64": { 28 | "version": "14.0.0", 29 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.0.0.tgz", 30 | "integrity": "sha512-HQKi159jCz4SRsPesVCiNN6tPSAFUkOuSkpJsqYTIlbHLKr1mD6be/J0TvWV6fwJekj81bZV9V/Tgx3C2HO9lA==", 31 | "cpu": [ 32 | "arm64" 33 | ], 34 | "optional": true, 35 | "os": [ 36 | "darwin" 37 | ], 38 | "engines": { 39 | "node": ">= 10" 40 | } 41 | }, 42 | "node_modules/@next/swc-darwin-x64": { 43 | "version": "14.0.0", 44 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.0.tgz", 45 | "integrity": "sha512-4YyQLMSaCgX/kgC1jjF3s3xSoBnwHuDhnF6WA1DWNEYRsbOOPWjcYhv8TKhRe2ApdOam+VfQSffC4ZD+X4u1Cg==", 46 | "cpu": [ 47 | "x64" 48 | ], 49 | "optional": true, 50 | "os": [ 51 | "darwin" 52 | ], 53 | "engines": { 54 | "node": ">= 10" 55 | } 56 | }, 57 | "node_modules/@next/swc-linux-arm64-gnu": { 58 | "version": "14.0.0", 59 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.0.tgz", 60 | "integrity": "sha512-io7fMkJ28Glj7SH8yvnlD6naIhRDnDxeE55CmpQkj3+uaA2Hko6WGY2pT5SzpQLTnGGnviK85cy8EJ2qsETj/g==", 61 | "cpu": [ 62 | "arm64" 63 | ], 64 | "optional": true, 65 | "os": [ 66 | "linux" 67 | ], 68 | "engines": { 69 | "node": ">= 10" 70 | } 71 | }, 72 | "node_modules/@next/swc-linux-arm64-musl": { 73 | "version": "14.0.0", 74 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.0.tgz", 75 | "integrity": "sha512-nC2h0l1Jt8LEzyQeSs/BKpXAMe0mnHIMykYALWaeddTqCv5UEN8nGO3BG8JAqW/Y8iutqJsaMe2A9itS0d/r8w==", 76 | "cpu": [ 77 | "arm64" 78 | ], 79 | "optional": true, 80 | "os": [ 81 | "linux" 82 | ], 83 | "engines": { 84 | "node": ">= 10" 85 | } 86 | }, 87 | "node_modules/@next/swc-linux-x64-gnu": { 88 | "version": "14.0.0", 89 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.0.tgz", 90 | "integrity": "sha512-Wf+WjXibJQ7hHXOdNOmSMW5bxeJHVf46Pwb3eLSD2L76NrytQlif9NH7JpHuFlYKCQGfKfgSYYre5rIfmnSwQw==", 91 | "cpu": [ 92 | "x64" 93 | ], 94 | "optional": true, 95 | "os": [ 96 | "linux" 97 | ], 98 | "engines": { 99 | "node": ">= 10" 100 | } 101 | }, 102 | "node_modules/@next/swc-linux-x64-musl": { 103 | "version": "14.0.0", 104 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.0.tgz", 105 | "integrity": "sha512-WTZb2G7B+CTsdigcJVkRxfcAIQj7Lf0ipPNRJ3vlSadU8f0CFGv/ST+sJwF5eSwIe6dxKoX0DG6OljDBaad+rg==", 106 | "cpu": [ 107 | "x64" 108 | ], 109 | "optional": true, 110 | "os": [ 111 | "linux" 112 | ], 113 | "engines": { 114 | "node": ">= 10" 115 | } 116 | }, 117 | "node_modules/@next/swc-win32-arm64-msvc": { 118 | "version": "14.0.0", 119 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.0.tgz", 120 | "integrity": "sha512-7R8/x6oQODmNpnWVW00rlWX90sIlwluJwcvMT6GXNIBOvEf01t3fBg0AGURNKdTJg2xNuP7TyLchCL7Lh2DTiw==", 121 | "cpu": [ 122 | "arm64" 123 | ], 124 | "optional": true, 125 | "os": [ 126 | "win32" 127 | ], 128 | "engines": { 129 | "node": ">= 10" 130 | } 131 | }, 132 | "node_modules/@next/swc-win32-ia32-msvc": { 133 | "version": "14.0.0", 134 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.0.tgz", 135 | "integrity": "sha512-RLK1nELvhCnxaWPF07jGU4x3tjbyx2319q43loZELqF0+iJtKutZ+Lk8SVmf/KiJkYBc7Cragadz7hb3uQvz4g==", 136 | "cpu": [ 137 | "ia32" 138 | ], 139 | "optional": true, 140 | "os": [ 141 | "win32" 142 | ], 143 | "engines": { 144 | "node": ">= 10" 145 | } 146 | }, 147 | "node_modules/@next/swc-win32-x64-msvc": { 148 | "version": "14.0.0", 149 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.0.tgz", 150 | "integrity": "sha512-g6hLf1SUko+hnnaywQQZzzb3BRecQsoKkF3o/C+F+dOA4w/noVAJngUVkfwF0+2/8FzNznM7ofM6TGZO9svn7w==", 151 | "cpu": [ 152 | "x64" 153 | ], 154 | "optional": true, 155 | "os": [ 156 | "win32" 157 | ], 158 | "engines": { 159 | "node": ">= 10" 160 | } 161 | }, 162 | "node_modules/@swc/helpers": { 163 | "version": "0.5.2", 164 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", 165 | "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", 166 | "dependencies": { 167 | "tslib": "^2.4.0" 168 | } 169 | }, 170 | "node_modules/@types/node": { 171 | "version": "20.11.25", 172 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz", 173 | "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==", 174 | "dev": true, 175 | "dependencies": { 176 | "undici-types": "~5.26.4" 177 | } 178 | }, 179 | "node_modules/@types/prop-types": { 180 | "version": "15.7.11", 181 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.11.tgz", 182 | "integrity": "sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==", 183 | "dev": true 184 | }, 185 | "node_modules/@types/react": { 186 | "version": "18.2.64", 187 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.64.tgz", 188 | "integrity": "sha512-MlmPvHgjj2p3vZaxbQgFUQFvD8QiZwACfGqEdDSWou5yISWxDQ4/74nCAwsUiX7UFLKZz3BbVSPj+YxeoGGCfg==", 189 | "dev": true, 190 | "dependencies": { 191 | "@types/prop-types": "*", 192 | "@types/scheduler": "*", 193 | "csstype": "^3.0.2" 194 | } 195 | }, 196 | "node_modules/@types/react-dom": { 197 | "version": "18.2.21", 198 | "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.21.tgz", 199 | "integrity": "sha512-gnvBA/21SA4xxqNXEwNiVcP0xSGHh/gi1VhWv9Bl46a0ItbTT5nFY+G9VSQpaG/8N/qdJpJ+vftQ4zflTtnjLw==", 200 | "dev": true, 201 | "dependencies": { 202 | "@types/react": "*" 203 | } 204 | }, 205 | "node_modules/@types/scheduler": { 206 | "version": "0.16.8", 207 | "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.8.tgz", 208 | "integrity": "sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==", 209 | "dev": true 210 | }, 211 | "node_modules/busboy": { 212 | "version": "1.6.0", 213 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 214 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 215 | "dependencies": { 216 | "streamsearch": "^1.1.0" 217 | }, 218 | "engines": { 219 | "node": ">=10.16.0" 220 | } 221 | }, 222 | "node_modules/caniuse-lite": { 223 | "version": "1.0.30001597", 224 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", 225 | "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", 226 | "funding": [ 227 | { 228 | "type": "opencollective", 229 | "url": "https://opencollective.com/browserslist" 230 | }, 231 | { 232 | "type": "tidelift", 233 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 234 | }, 235 | { 236 | "type": "github", 237 | "url": "https://github.com/sponsors/ai" 238 | } 239 | ] 240 | }, 241 | "node_modules/client-only": { 242 | "version": "0.0.1", 243 | "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 244 | "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" 245 | }, 246 | "node_modules/csstype": { 247 | "version": "3.1.3", 248 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 249 | "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", 250 | "dev": true 251 | }, 252 | "node_modules/glob-to-regexp": { 253 | "version": "0.4.1", 254 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 255 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" 256 | }, 257 | "node_modules/graceful-fs": { 258 | "version": "4.2.11", 259 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 260 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 261 | }, 262 | "node_modules/js-tokens": { 263 | "version": "4.0.0", 264 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 265 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" 266 | }, 267 | "node_modules/loose-envify": { 268 | "version": "1.4.0", 269 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 270 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 271 | "dependencies": { 272 | "js-tokens": "^3.0.0 || ^4.0.0" 273 | }, 274 | "bin": { 275 | "loose-envify": "cli.js" 276 | } 277 | }, 278 | "node_modules/nanoid": { 279 | "version": "3.3.7", 280 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", 281 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", 282 | "funding": [ 283 | { 284 | "type": "github", 285 | "url": "https://github.com/sponsors/ai" 286 | } 287 | ], 288 | "bin": { 289 | "nanoid": "bin/nanoid.cjs" 290 | }, 291 | "engines": { 292 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 293 | } 294 | }, 295 | "node_modules/next": { 296 | "version": "14.0.0", 297 | "resolved": "https://registry.npmjs.org/next/-/next-14.0.0.tgz", 298 | "integrity": "sha512-J0jHKBJpB9zd4+c153sair0sz44mbaCHxggs8ryVXSFBuBqJ8XdE9/ozoV85xGh2VnSjahwntBZZgsihL9QznA==", 299 | "dependencies": { 300 | "@next/env": "14.0.0", 301 | "@swc/helpers": "0.5.2", 302 | "busboy": "1.6.0", 303 | "caniuse-lite": "^1.0.30001406", 304 | "postcss": "8.4.31", 305 | "styled-jsx": "5.1.1", 306 | "watchpack": "2.4.0" 307 | }, 308 | "bin": { 309 | "next": "dist/bin/next" 310 | }, 311 | "engines": { 312 | "node": ">=18.17.0" 313 | }, 314 | "optionalDependencies": { 315 | "@next/swc-darwin-arm64": "14.0.0", 316 | "@next/swc-darwin-x64": "14.0.0", 317 | "@next/swc-linux-arm64-gnu": "14.0.0", 318 | "@next/swc-linux-arm64-musl": "14.0.0", 319 | "@next/swc-linux-x64-gnu": "14.0.0", 320 | "@next/swc-linux-x64-musl": "14.0.0", 321 | "@next/swc-win32-arm64-msvc": "14.0.0", 322 | "@next/swc-win32-ia32-msvc": "14.0.0", 323 | "@next/swc-win32-x64-msvc": "14.0.0" 324 | }, 325 | "peerDependencies": { 326 | "@opentelemetry/api": "^1.1.0", 327 | "react": "^18.2.0", 328 | "react-dom": "^18.2.0", 329 | "sass": "^1.3.0" 330 | }, 331 | "peerDependenciesMeta": { 332 | "@opentelemetry/api": { 333 | "optional": true 334 | }, 335 | "sass": { 336 | "optional": true 337 | } 338 | } 339 | }, 340 | "node_modules/picocolors": { 341 | "version": "1.0.0", 342 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 343 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 344 | }, 345 | "node_modules/postcss": { 346 | "version": "8.4.31", 347 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", 348 | "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", 349 | "funding": [ 350 | { 351 | "type": "opencollective", 352 | "url": "https://opencollective.com/postcss/" 353 | }, 354 | { 355 | "type": "tidelift", 356 | "url": "https://tidelift.com/funding/github/npm/postcss" 357 | }, 358 | { 359 | "type": "github", 360 | "url": "https://github.com/sponsors/ai" 361 | } 362 | ], 363 | "dependencies": { 364 | "nanoid": "^3.3.6", 365 | "picocolors": "^1.0.0", 366 | "source-map-js": "^1.0.2" 367 | }, 368 | "engines": { 369 | "node": "^10 || ^12 || >=14" 370 | } 371 | }, 372 | "node_modules/react": { 373 | "version": "18.2.0", 374 | "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", 375 | "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", 376 | "dependencies": { 377 | "loose-envify": "^1.1.0" 378 | }, 379 | "engines": { 380 | "node": ">=0.10.0" 381 | } 382 | }, 383 | "node_modules/react-dom": { 384 | "version": "18.2.0", 385 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", 386 | "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", 387 | "dependencies": { 388 | "loose-envify": "^1.1.0", 389 | "scheduler": "^0.23.0" 390 | }, 391 | "peerDependencies": { 392 | "react": "^18.2.0" 393 | } 394 | }, 395 | "node_modules/scheduler": { 396 | "version": "0.23.0", 397 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", 398 | "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", 399 | "dependencies": { 400 | "loose-envify": "^1.1.0" 401 | } 402 | }, 403 | "node_modules/source-map-js": { 404 | "version": "1.0.2", 405 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 406 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 407 | "engines": { 408 | "node": ">=0.10.0" 409 | } 410 | }, 411 | "node_modules/streamsearch": { 412 | "version": "1.1.0", 413 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 414 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", 415 | "engines": { 416 | "node": ">=10.0.0" 417 | } 418 | }, 419 | "node_modules/styled-jsx": { 420 | "version": "5.1.1", 421 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", 422 | "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", 423 | "dependencies": { 424 | "client-only": "0.0.1" 425 | }, 426 | "engines": { 427 | "node": ">= 12.0.0" 428 | }, 429 | "peerDependencies": { 430 | "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" 431 | }, 432 | "peerDependenciesMeta": { 433 | "@babel/core": { 434 | "optional": true 435 | }, 436 | "babel-plugin-macros": { 437 | "optional": true 438 | } 439 | } 440 | }, 441 | "node_modules/tslib": { 442 | "version": "2.6.2", 443 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", 444 | "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" 445 | }, 446 | "node_modules/typescript": { 447 | "version": "5.4.2", 448 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", 449 | "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", 450 | "dev": true, 451 | "bin": { 452 | "tsc": "bin/tsc", 453 | "tsserver": "bin/tsserver" 454 | }, 455 | "engines": { 456 | "node": ">=14.17" 457 | } 458 | }, 459 | "node_modules/undici-types": { 460 | "version": "5.26.5", 461 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", 462 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", 463 | "dev": true 464 | }, 465 | "node_modules/watchpack": { 466 | "version": "2.4.0", 467 | "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", 468 | "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", 469 | "dependencies": { 470 | "glob-to-regexp": "^0.4.1", 471 | "graceful-fs": "^4.1.2" 472 | }, 473 | "engines": { 474 | "node": ">=10.13.0" 475 | } 476 | } 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /examples/nextjs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nextjs", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "react": "^18", 13 | "react-dom": "^18", 14 | "next": "14.0.0" 15 | }, 16 | "devDependencies": { 17 | "typescript": "^5", 18 | "@types/node": "^20", 19 | "@types/react": "^18", 20 | "@types/react-dom": "^18" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/nextjs/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/nextjs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | -------------------------------------------------------------------------------- /examples/serverless-otel/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # sst 5 | .sst 6 | .build 7 | 8 | # opennext 9 | .open-next 10 | 11 | # misc 12 | .DS_Store 13 | 14 | # local env files 15 | .env*.local 16 | -------------------------------------------------------------------------------- /examples/serverless-otel/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug SST Start", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", 9 | "runtimeArgs": ["start", "--increase-timeout"], 10 | "console": "integratedTerminal", 11 | "skipFiles": ["/**"], 12 | "env": {} 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/serverless-otel/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.sst": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/serverless-otel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "serverless-otel", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "sst dev", 8 | "build": "sst build", 9 | "deploy": "sst deploy --stage prod", 10 | "remove": "sst remove", 11 | "console": "sst console", 12 | "typecheck": "tsc --noEmit" 13 | }, 14 | "devDependencies": { 15 | "@tsconfig/node18": "^18.2.2", 16 | "@types/aws-lambda": "^8.10.134", 17 | "aws-cdk-lib": "2.95.1", 18 | "constructs": "10.2.69", 19 | "sst": "^2.40.3", 20 | "typescript": "^5.3.3" 21 | }, 22 | "workspaces": [ 23 | "packages/*" 24 | ], 25 | "dependencies": { 26 | "@aws-sdk/client-s3": "^3.523.0", 27 | "@opentelemetry/instrumentation-aws-sdk": "^0.38.1", 28 | "@opentelemetry/instrumentation-fetch": "^0.48.0", 29 | "axios": "^1.6.7", 30 | "form-data": "^4.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/serverless-otel/packages/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serverless-otel/functions", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "test": "sst bind vitest", 7 | "typecheck": "tsc -noEmit" 8 | }, 9 | "devDependencies": { 10 | "@types/node": "^20.11.20", 11 | "@types/aws-lambda": "^8.10.134", 12 | "vitest": "^1.3.1", 13 | "sst": "^2.40.3" 14 | } 15 | } -------------------------------------------------------------------------------- /examples/serverless-otel/packages/functions/src/lambda.ts: -------------------------------------------------------------------------------- 1 | import { withOpenTelemetry } from "./tracing"; 2 | import { trace } from "@opentelemetry/api"; 3 | 4 | import { S3 } from "@aws-sdk/client-s3"; 5 | import axios from 'axios'; 6 | import qs from 'node:querystring'; 7 | import FormData from 'form-data'; 8 | import { APIGatewayProxyEventV2 } from "aws-lambda"; 9 | 10 | const s3 = new S3({}); 11 | 12 | const tracer = trace.getTracer('example'); 13 | export const handler = withOpenTelemetry(async (e: APIGatewayProxyEventV2) => { 14 | const span = tracer.startSpan('example'); 15 | 16 | await s3.listBuckets({}); 17 | await s3.putObject({ Bucket: process.env.CAT_PICTURES_BUCKET, Key: 'cat.png', Body: 'example' }); 18 | await s3.getObject({ Bucket: process.env.CAT_PICTURES_BUCKET, Key: 'cat.png' }); 19 | await new Promise((resolve) => setTimeout(resolve, 1000)); 20 | if(Math.random() > 0.5) { 21 | throw new Error('Random error'); 22 | } 23 | span.end() 24 | return { 25 | statusCode: 200, 26 | body: JSON.stringify({ 27 | message: 'Hello from Lambda!', 28 | }) 29 | }; 30 | }, { 31 | captureEvent: false, 32 | captureResponse: false, 33 | proactiveInitializationThreshold: 1000, 34 | timeoutThreshold: 500, 35 | extractContext(service, event) { 36 | console.log('Extracting context', service, event); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /examples/serverless-otel/packages/functions/src/tracing.ts: -------------------------------------------------------------------------------- 1 | import { BaselimeSDK, BetterHttpInstrumentation } from '../../../../../src/index' 2 | import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk'; 3 | import { withOpenTelemetry } from '../../../../../src/lambda'; 4 | import { flatten } from 'flat' 5 | 6 | const blockedRequestOperations = [ 7 | { service: 'S3', operation: 'PutObject' }, 8 | { service: 'Kinesis', operation: 'PutRecord' } 9 | ] 10 | 11 | const blockedResponseOperations = [ 12 | { service: 'S3', operation: 'GetObject'}, 13 | ] 14 | 15 | 16 | new BaselimeSDK({ 17 | baselimeKey: process.env.BASELIME_KEY, collectorUrl: 'https://otel.baselime.cc/v1', serverless: false, instrumentations: [ 18 | new AwsInstrumentation({ 19 | suppressInternalInstrumentation: true, 20 | responseHook(span, { response }) { 21 | if(response && !blockedResponseOperations.some(({ service, operation }) => response.request.serviceName === service && response.request.commandName === operation)){ 22 | span.setAttributes(flatten({ 23 | response: response.data, 24 | })) 25 | } 26 | }, 27 | preRequestHook(span, request) { 28 | 29 | if(!blockedRequestOperations.some(({ service, operation }) => request.request.serviceName === service && request.request.commandName === operation)){ 30 | span.setAttributes(flatten({ 31 | request: request.request, 32 | })) 33 | } 34 | } 35 | }), 36 | new BetterHttpInstrumentation({ 37 | captureBody: true, 38 | captureHeaders: true, 39 | }) 40 | ], 41 | resourceDetectors: [], 42 | }).start(); 43 | 44 | export { 45 | withOpenTelemetry 46 | } -------------------------------------------------------------------------------- /examples/serverless-otel/packages/functions/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/serverless-otel/packages/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@serverless-otel/core/*": ["../core/src/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/serverless-otel/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/**/*" 3 | -------------------------------------------------------------------------------- /examples/serverless-otel/sst.config.ts: -------------------------------------------------------------------------------- 1 | import { SSTConfig } from "sst"; 2 | import { API } from "./stacks/MyStack"; 3 | 4 | export default { 5 | config(_input) { 6 | return { 7 | name: "serverless-otel", 8 | region: "us-east-1", 9 | }; 10 | }, 11 | stacks(app) { 12 | app.stack(API); 13 | } 14 | } satisfies SSTConfig; 15 | -------------------------------------------------------------------------------- /examples/serverless-otel/stacks/MyStack.ts: -------------------------------------------------------------------------------- 1 | import { StackContext, Api, EventBus, Bucket } from "sst/constructs"; 2 | 3 | export function API({ stack }: StackContext) { 4 | const bus = new EventBus(stack, "bus", { 5 | defaults: { 6 | retries: 10, 7 | }, 8 | }); 9 | 10 | const catPictures = new Bucket(stack, "cat-pictures") 11 | 12 | const api = new Api(stack, "api", { 13 | defaults: { 14 | function: { 15 | runtime: "nodejs16.x", 16 | functionName: "api", 17 | nodejs: { 18 | format: 'cjs', 19 | install: ["@smithy/middleware-stack", "@aws-sdk/middleware-stack"] 20 | }, 21 | timeout: 5, 22 | bind: [bus], 23 | environment: { 24 | BASELIME_KEY: process.env.BASELIME_KEY || '', 25 | CAT_PICTURES_BUCKET: catPictures.bucketName 26 | } 27 | }, 28 | }, 29 | routes: { 30 | "GET /": "packages/functions/src/lambda.handler", 31 | }, 32 | }); 33 | 34 | api.attachPermissions(["s3"]); 35 | 36 | stack.addOutputs({ 37 | ApiEndpoint: api.url, 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /examples/serverless-otel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "exclude": ["packages"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "moduleResolution": "node" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/sst-service/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .sst -------------------------------------------------------------------------------- /examples/sst-service/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # sst 5 | .sst 6 | .build 7 | 8 | # opennext 9 | .open-next 10 | 11 | # misc 12 | .DS_Store 13 | 14 | # local env files 15 | .env*.local 16 | -------------------------------------------------------------------------------- /examples/sst-service/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug SST Start", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", 9 | "runtimeArgs": ["start", "--increase-timeout"], 10 | "console": "integratedTerminal", 11 | "skipFiles": ["/**"], 12 | "env": {} 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/sst-service/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.sst": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/sst-service/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18 2 | 3 | # Set the working directory inside the container 4 | WORKDIR /usr/src/app 5 | 6 | # Copy package.json and package-lock.json to the working directory 7 | COPY package*.json ./ 8 | 9 | # Install app dependencies 10 | RUN npm install --ci --omit=dev 11 | 12 | # Copy the rest of the application code to the working directory 13 | COPY src src 14 | 15 | # Command to run your app when the container starts 16 | CMD ["node","src/index.js" ] 17 | -------------------------------------------------------------------------------- /examples/sst-service/cdk.context.json: -------------------------------------------------------------------------------- 1 | { 2 | "availability-zones:account=522104763258:region=eu-west-2": [ 3 | "eu-west-2a", 4 | "eu-west-2b", 5 | "eu-west-2c" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /examples/sst-service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sst-service", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "sst dev", 8 | "build": "sst build", 9 | "deploy": "sst deploy", 10 | "remove": "sst remove", 11 | "console": "sst console", 12 | "typecheck": "tsc --noEmit" 13 | }, 14 | "devDependencies": { 15 | "@tsconfig/node16": "^16.1.0", 16 | "constructs": "10.1.156", 17 | "sst": "^2.24.7", 18 | "typescript": "^5.1.6" 19 | }, 20 | "workspaces": [ 21 | "packages/*" 22 | ], 23 | "dependencies": { 24 | "@baselime/node-opentelemetry": "^0.2.1", 25 | "@opentelemetry/auto-instrumentations-node": "^0.39.1", 26 | "aws-cdk-lib": "^2.91.0", 27 | "express": "^4.18.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /examples/sst-service/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/**/*" 3 | -------------------------------------------------------------------------------- /examples/sst-service/src/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | const app = express(); 3 | 4 | const port = process.env.PORT || 3000; 5 | 6 | app.get("/", function (req, res) { 7 | res.send("Hello World!"); 8 | }); 9 | 10 | app.listen(port, function () { 11 | console.log(`Example app listening on port ${port}!`); 12 | }); 13 | -------------------------------------------------------------------------------- /examples/sst-service/src/tracing.cjs: -------------------------------------------------------------------------------- 1 | const { BaselimeSDK } = require('@baselime/node-opentelemetry'); 2 | const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node'); 3 | 4 | 5 | const sdk = new BaselimeSDK({ 6 | instrumentations: [ 7 | getNodeAutoInstrumentations({ 8 | '@opentelemetry/instrumentation-http': { 9 | ignoreIncomingRequestHook(request) { 10 | if(request.headers['user-agent']?.includes('HealthChecker')) { 11 | return true 12 | } 13 | return false 14 | } 15 | } 16 | }), 17 | ], 18 | }); 19 | 20 | sdk.start(); -------------------------------------------------------------------------------- /examples/sst-service/sst.config.ts: -------------------------------------------------------------------------------- 1 | import { SSTConfig } from "sst"; 2 | import { API } from "./stacks/MyStack"; 3 | 4 | export default { 5 | config(_input) { 6 | return { 7 | name: "sst-service", 8 | region: "eu-west-2", 9 | }; 10 | }, 11 | stacks(app) { 12 | app.stack(API); 13 | } 14 | } satisfies SSTConfig; 15 | -------------------------------------------------------------------------------- /examples/sst-service/stacks/MyStack.ts: -------------------------------------------------------------------------------- 1 | import { StackContext, Service } from "sst/constructs"; 2 | import { StringParameter } from 'aws-cdk-lib/aws-ssm'; 3 | import * as ecs from 'aws-cdk-lib/aws-ecs'; 4 | 5 | function addLoggingConfig(service: Service, key: string) { 6 | console.log(service.node.children) 7 | 8 | } 9 | export function API({ stack }: StackContext) { 10 | const key = StringParameter.valueForStringParameter(stack, 'baselime-key'); 11 | 12 | 13 | const service = new Service(stack, 'sst-service', { 14 | path: './', 15 | environment: { 16 | BASELIME_KEY: key, 17 | NODE_OPTIONS: "-r ./src/tracing.cjs --experimental-loader=import-in-the-middle/hook.mjs" 18 | }, 19 | cdk: { 20 | container: { 21 | logging: new ecs.FireLensLogDriver({ 22 | options: { 23 | "Name": "http", 24 | "Host": "ecs-logs-ingest.baselime.io", 25 | "Port": "443", 26 | "TLS": "on", 27 | "format": "json", 28 | "retry_limit": "2", 29 | "header": `x-api-key ${key}`, 30 | }, 31 | }), 32 | } 33 | } 34 | }); 35 | 36 | addLoggingConfig(service, key) 37 | stack.addOutputs({ 38 | URL: service.url 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /examples/sst-service/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node16/tsconfig.json", 3 | "exclude": ["packages"], 4 | "compilerOptions": { 5 | "moduleResolution": "node" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /examples/state-machine/.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # sst 5 | .sst 6 | .build 7 | 8 | # opennext 9 | .open-next 10 | 11 | # misc 12 | .DS_Store 13 | 14 | # local env files 15 | .env*.local 16 | -------------------------------------------------------------------------------- /examples/state-machine/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug SST Start", 6 | "type": "node", 7 | "request": "launch", 8 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", 9 | "runtimeArgs": ["start", "--increase-timeout"], 10 | "console": "integratedTerminal", 11 | "skipFiles": ["/**"], 12 | "env": {} 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/state-machine/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/.sst": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/state-machine/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "state-machine", 3 | "version": "0.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "sst dev", 8 | "build": "sst build", 9 | "deploy": "sst deploy", 10 | "remove": "sst remove", 11 | "console": "sst console", 12 | "typecheck": "tsc --noEmit" 13 | }, 14 | "devDependencies": { 15 | "sst": "^2.41.4", 16 | "aws-cdk-lib": "2.132.1", 17 | "constructs": "10.3.0", 18 | "typescript": "^5.4.3", 19 | "@tsconfig/node18": "^18.2.3" 20 | }, 21 | "workspaces": [ 22 | "packages/*" 23 | ] 24 | } -------------------------------------------------------------------------------- /examples/state-machine/packages/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@state-machine/functions", 3 | "version": "0.0.0", 4 | "type": "module", 5 | "scripts": { 6 | "test": "sst bind vitest", 7 | "typecheck": "tsc -noEmit" 8 | }, 9 | "devDependencies": { 10 | "@types/node": "^20.11.30", 11 | "@types/aws-lambda": "^8.10.136", 12 | "vitest": "^1.4.0", 13 | "sst": "^2.41.4" 14 | } 15 | } -------------------------------------------------------------------------------- /examples/state-machine/packages/functions/src/lambda.ts: -------------------------------------------------------------------------------- 1 | import { withOpenTelemetry } from "./tracing"; 2 | import { ApiHandler } from "sst/node/api"; 3 | 4 | export const handler = withOpenTelemetry(ApiHandler(async (_evt) => { 5 | return { 6 | statusCode: 200, 7 | body: `Hello world. The time is ${new Date().toISOString()}`, 8 | }; 9 | })); 10 | -------------------------------------------------------------------------------- /examples/state-machine/packages/functions/src/task-one.ts: -------------------------------------------------------------------------------- 1 | import { withOpenTelemetry } from "./tracing"; 2 | 3 | export const handler = async (_,__, callback) => { 4 | callback(null, { 5 | statusCode: 200, 6 | body: `Hello world. The time is ${new Date().toISOString()}`, 7 | }); 8 | }; 9 | -------------------------------------------------------------------------------- /examples/state-machine/packages/functions/src/task-three.ts: -------------------------------------------------------------------------------- 1 | import { withOpenTelemetry } from "./tracing"; 2 | 3 | export const handler = withOpenTelemetry(async (_evt) => { 4 | return { 5 | statusCode: 200, 6 | body: `Hello world. The time is ${new Date().toISOString()}`, 7 | }; 8 | }); 9 | -------------------------------------------------------------------------------- /examples/state-machine/packages/functions/src/task-two.ts: -------------------------------------------------------------------------------- 1 | 2 | export const handler = async (_evt) => { 3 | console.log(_evt) 4 | return { 5 | statusCode: 200, 6 | body: `Hello world. The time is ${new Date().toISOString()}`, 7 | }; 8 | }; 9 | -------------------------------------------------------------------------------- /examples/state-machine/packages/functions/src/tracing.ts: -------------------------------------------------------------------------------- 1 | import { BaselimeSDK, BetterHttpInstrumentation } from '../../../../../src/index' 2 | import { withOpenTelemetry } from '../../../../../src/lambda'; 3 | 4 | new BaselimeSDK({ 5 | baselimeKey: '', collectorUrl: 'https://otel.baselime.cc/v1', serverless: false, instrumentations: [ 6 | new BetterHttpInstrumentation({ 7 | captureBody: true, 8 | captureHeaders: true, 9 | }) 10 | ], 11 | resourceDetectors: [], 12 | }).start(); 13 | 14 | export { 15 | withOpenTelemetry 16 | } -------------------------------------------------------------------------------- /examples/state-machine/packages/functions/sst-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/state-machine/packages/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "compilerOptions": { 4 | "module": "esnext", 5 | "moduleResolution": "node", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@state-machine/core/*": ["../core/src/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/state-machine/pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "packages/**/*" 3 | -------------------------------------------------------------------------------- /examples/state-machine/sst.config.ts: -------------------------------------------------------------------------------- 1 | import { SSTConfig } from "sst"; 2 | import { API } from "./stacks/MyStack"; 3 | 4 | export default { 5 | config(_input) { 6 | return { 7 | name: "state-machine", 8 | region: "us-east-1", 9 | }; 10 | }, 11 | stacks(app) { 12 | app.stack(API); 13 | } 14 | } satisfies SSTConfig; 15 | -------------------------------------------------------------------------------- /examples/state-machine/stacks/MyStack.ts: -------------------------------------------------------------------------------- 1 | import { StackContext, Function } from "sst/constructs"; 2 | import { LambdaInvoke, SnsPublish } from "aws-cdk-lib/aws-stepfunctions-tasks"; 3 | import { Chain, Parallel, StateMachine, TaskInput} from "aws-cdk-lib/aws-stepfunctions"; 4 | export function API({ stack }: StackContext) { 5 | 6 | stack.addDefaultFunctionEnv({ 7 | BASELIME_TRACE_STEP_FUNCTION: "true" 8 | }) 9 | const taskOne = new LambdaInvoke(stack, "TaskOne", { 10 | lambdaFunction: new Function(stack, "task-one", { 11 | handler: "packages/functions/src/task-one.handler", 12 | }), 13 | 14 | }) 15 | 16 | const taskTwoA = new LambdaInvoke(stack, "TaskTwoA", { 17 | lambdaFunction: new Function(stack, "task-two-a", { 18 | handler: "packages/functions/src/task-two.handler", 19 | }), 20 | payload: TaskInput.fromObject({ 21 | code: TaskInput.fromJsonPathAt("$.Payload.statusCode").value, 22 | _baselime: TaskInput.fromJsonPathAt("$.Payload.['_baselime', 'null']").value 23 | }) 24 | }) 25 | 26 | const taskTwoB = new LambdaInvoke(stack, "TaskTwoB", { 27 | lambdaFunction: new Function(stack, "task-two-b", { 28 | handler: "packages/functions/src/task-two.handler", 29 | }), 30 | }) 31 | 32 | const taskThree = new LambdaInvoke(stack, "TaskThree", { 33 | lambdaFunction: new Function(stack, "task-three", { 34 | handler: "packages/functions/src/task-three.handler", 35 | }), 36 | }) 37 | 38 | const parallel = new Parallel(stack, "ParallelCompute"); 39 | const stateDefinition = Chain.start(taskOne).next(parallel.branch(taskTwoA).branch(taskTwoB)).next(taskThree) 40 | 41 | 42 | const stateMachine = new StateMachine(stack, "MyStateMachine", { 43 | definition: stateDefinition, 44 | }); 45 | 46 | 47 | stack.addOutputs({ 48 | stateMachine: stateMachine.stateMachineName 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /examples/state-machine/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/node18/tsconfig.json", 3 | "exclude": ["packages"], 4 | "compilerOptions": { 5 | "module": "esnext", 6 | "moduleResolution": "node" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@baselime/node-opentelemetry", 3 | "version": "0.5.8", 4 | "description": "Instrument node.js applications with open telemetry ", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "types": "./dist/index.d.cts", 9 | "default": "./dist/index.cjs" 10 | }, 11 | "./lambda": { 12 | "types": "./dist/lambda.d.cts", 13 | "default": "./dist/lambda.cjs" 14 | }, 15 | "./trpc": { 16 | "types": "./dist/trpc.d.cts", 17 | "default": "./dist/trpc.cjs" 18 | } 19 | }, 20 | "files": [ 21 | "dist" 22 | ], 23 | "devDependencies": { 24 | "@release-it/keep-a-changelog": "^4.0.0", 25 | "@types/node": "^20.8.6", 26 | "esbuild": "^0.19.4", 27 | "mockttp": "^3.9.4", 28 | "patch-package": "^8.0.0", 29 | "release-it": "^16.2.1", 30 | "tsup": "^7.2.0", 31 | "typescript": "^5.2.2", 32 | "vitest": "^0.34.6" 33 | }, 34 | "scripts": { 35 | "check": "tsc --noEmit", 36 | "build": "tsup", 37 | "test": "vitest", 38 | "release": "release-it" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/baselime/node-opentelemetry.git" 43 | }, 44 | "keywords": [ 45 | "baselime", 46 | "otel", 47 | "opentelemetry", 48 | "tracing", 49 | "node.js" 50 | ], 51 | "author": "", 52 | "license": "MIT", 53 | "bugs": { 54 | "url": "https://github.com/baselime/node-opentelemetry/issues" 55 | }, 56 | "homepage": "https://github.com/baselime/node-opentelemetry#readme", 57 | "peerDependencies": { 58 | "@trpc/server": "^10.0.0 || ^11.0.0" 59 | }, 60 | "dependencies": { 61 | "@opentelemetry/api": "^1.8.0", 62 | "@opentelemetry/exporter-trace-otlp-http": "^0.50.0", 63 | "@opentelemetry/instrumentation": "^0.50.0", 64 | "@opentelemetry/instrumentation-http": "^0.50.0", 65 | "@opentelemetry/resource-detector-aws": "^1.4.1", 66 | "@opentelemetry/resources": "^1.23.0", 67 | "@opentelemetry/sdk-node": "^0.50.0", 68 | "@opentelemetry/sdk-trace-node": "^1.23.0", 69 | "@types/aws-lambda": "^8.10.136", 70 | "axios": "^1.6.8", 71 | "flat": "^6.0.1", 72 | "undici": "^6.11.1" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /patches/@opentelemetry+instrumentation+0.48.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@opentelemetry/instrumentation/build/esm/platform/node/instrumentation.js b/node_modules/@opentelemetry/instrumentation/build/esm/platform/node/instrumentation.js 2 | index f497f63..4554ab2 100644 3 | --- a/node_modules/@opentelemetry/instrumentation/build/esm/platform/node/instrumentation.js 4 | +++ b/node_modules/@opentelemetry/instrumentation/build/esm/platform/node/instrumentation.js 5 | @@ -45,7 +45,7 @@ import { satisfies } from 'semver'; 6 | import { wrap, unwrap } from 'shimmer'; 7 | import { InstrumentationAbstract } from '../../instrumentation'; 8 | import { RequireInTheMiddleSingleton, } from './RequireInTheMiddleSingleton'; 9 | -import * as ImportInTheMiddle from 'import-in-the-middle'; 10 | +import ImportInTheMiddle from 'import-in-the-middle'; 11 | import { diag } from '@opentelemetry/api'; 12 | import { Hook } from 'require-in-the-middle'; 13 | /** 14 | -------------------------------------------------------------------------------- /patches/@opentelemetry+instrumentation+0.50.0.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/@opentelemetry/instrumentation/build/esm/platform/node/instrumentation.js b/node_modules/@opentelemetry/instrumentation/build/esm/platform/node/instrumentation.js 2 | index 3a717ca..d8e2404 100644 3 | --- a/node_modules/@opentelemetry/instrumentation/build/esm/platform/node/instrumentation.js 4 | +++ b/node_modules/@opentelemetry/instrumentation/build/esm/platform/node/instrumentation.js 5 | @@ -45,7 +45,7 @@ import { satisfies } from 'semver'; 6 | import { wrap, unwrap } from 'shimmer'; 7 | import { InstrumentationAbstract } from '../../instrumentation'; 8 | import { RequireInTheMiddleSingleton, } from './RequireInTheMiddleSingleton'; 9 | -import * as ImportInTheMiddle from 'import-in-the-middle'; 10 | +import ImportInTheMiddle from 'import-in-the-middle'; 11 | import { diag } from '@opentelemetry/api'; 12 | import { Hook } from 'require-in-the-middle'; 13 | /** 14 | -------------------------------------------------------------------------------- /src/baselime.ts: -------------------------------------------------------------------------------- 1 | import { BatchSpanProcessor, NodeTracerProvider, SimpleSpanProcessor, Sampler, ConsoleSpanExporter } from '@opentelemetry/sdk-trace-node' 2 | import api, { Attributes, DiagConsoleLogger, DiagLogLevel } from "@opentelemetry/api"; 3 | import { DetectorSync, detectResourcesSync, Resource, ResourceAttributes } from '@opentelemetry/resources'; 4 | import { awsLambdaDetector } from '@opentelemetry/resource-detector-aws' 5 | import { VercelDetector } from './resources/vercel.ts'; 6 | import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; 7 | import { InstrumentationOption, registerInstrumentations } from '@opentelemetry/instrumentation'; 8 | import { existsSync } from 'fs'; 9 | import { ServiceDetector } from './resources/service.ts'; 10 | import { KoyebDetector } from './resources/koyeb.ts'; 11 | 12 | type BaselimeSDKOpts = { 13 | instrumentations?: InstrumentationOption[], 14 | collectorUrl?: string, 15 | baselimeKey?: string, 16 | service?: string, 17 | log?: boolean, 18 | namespace?: string, 19 | serverless?: boolean 20 | sampler?: Sampler 21 | resourceDetectors?: DetectorSync[], 22 | resourceAttributes?: Resource | Attributes 23 | } 24 | 25 | 26 | /** 27 | * BaselimeSDK is a wrapper around the OpenTelemetry NodeSDK that configures it to send traces to Baselime. 28 | * 29 | * @param {InstrumentationOption[]} options.instrumentations - The OpenTelemetry instrumentations to enable. 30 | * @param {string} options.baselimeKey - The Baselime API key. Defaults to the BASELIME_KEY environment variable. 31 | * @param {string} options.service - The name of the service. 32 | * @param {string} options.namespace - The namespace of the service. 33 | * @param {boolean} options.serverless - Whether or not the service is running in a serverless environment. Defaults to false. 34 | * @param {boolean} options.log - Whether or not to enable the log exporter. Defaults to false. 35 | * @param {string} options.collectorUrl - The URL of the Baselime collector. Defaults to https://otel.baselime.io/v1 36 | * @param {Sampler} options.sampler - The OpenTelemetry sampler to use. Defaults to No Sampling. 37 | */ 38 | export class BaselimeSDK { 39 | options: BaselimeSDKOpts; 40 | attributes: ResourceAttributes; 41 | constructor(options: BaselimeSDKOpts) { 42 | options.serverless = options.serverless || false; 43 | options.collectorUrl = options.collectorUrl || process.env.COLLECTOR_URL || "https://otel.baselime.io/v1"; 44 | options.baselimeKey = options.baselimeKey || process.env.BASELIME_API_KEY || process.env.BASELIME_KEY 45 | 46 | this.options = options; 47 | } 48 | 49 | start() { 50 | if (process.env.OTEL_LOG_LEVEL === "debug") { 51 | api.diag.setLogger(new DiagConsoleLogger(), DiagLogLevel.ALL); 52 | } 53 | const provider = new NodeTracerProvider({ 54 | sampler: this.options.sampler, 55 | resource: detectResourcesSync({ 56 | detectors: [ 57 | awsLambdaDetector, 58 | new VercelDetector(), 59 | new KoyebDetector(), 60 | ...(this.options.resourceDetectors || []), 61 | new ServiceDetector({ serviceName: this.options.service, attributes: this.options.resourceAttributes }) 62 | ], 63 | }), 64 | forceFlushTimeoutMillis: 5000, 65 | }); 66 | 67 | 68 | 69 | // configure exporters 70 | 71 | let exporter: OTLPTraceExporter | ConsoleSpanExporter | undefined = undefined; 72 | 73 | if(!this.options.baselimeKey) { 74 | console.warn("No Baselime API key provided. Traces will not be sent to Baselime.") 75 | } 76 | 77 | 78 | if (this.options.baselimeKey) { 79 | let collectorUrl = this.options.collectorUrl; 80 | 81 | // If the baselime extension is running, we need to use the sandbox collector. 82 | if (existsSync('/opt/extensions/baselime')) { 83 | collectorUrl = 'http://sandbox:4323/otel'; 84 | } 85 | 86 | exporter = new OTLPTraceExporter({ 87 | url: collectorUrl, 88 | headers: { 89 | "x-api-key": this.options.baselimeKey || process.env.BASELIME_KEY || process.env.BASELIME_OTEL_KEY, 90 | }, 91 | timeoutMillis: 1000, 92 | }); 93 | } 94 | 95 | if (this.options.log) { 96 | exporter = new ConsoleSpanExporter(); 97 | } 98 | 99 | if(exporter) { 100 | const spanProcessor = this.options.serverless ? new SimpleSpanProcessor(exporter) : new BatchSpanProcessor(exporter, { 101 | maxQueueSize: 100, 102 | maxExportBatchSize: 5, 103 | }); 104 | 105 | 106 | provider.addSpanProcessor(spanProcessor); 107 | } 108 | 109 | provider.register(); 110 | 111 | registerInstrumentations({ 112 | instrumentations: [ 113 | ...this.options.instrumentations || [] 114 | ] 115 | }); 116 | return provider; 117 | } 118 | } -------------------------------------------------------------------------------- /src/http-plugins/plugin.ts: -------------------------------------------------------------------------------- 1 | import { ClientRequest, IncomingMessage, ServerResponse } from "http"; 2 | 3 | export class HttpPlugin { 4 | parseIncommingMessage?(request: IncomingMessage): Record; 5 | parseClientRequest?(request: ClientRequest): Record; 6 | captureBody = false 7 | name = 'base-plugin-should-extend' 8 | constructor() { 9 | 10 | } 11 | 12 | shouldParseRequest(request: ClientRequest | IncomingMessage): boolean { 13 | return false; 14 | } 15 | shouldParseResponse(response: IncomingMessage | ServerResponse): boolean { 16 | return false 17 | } 18 | } -------------------------------------------------------------------------------- /src/http-plugins/stripe.ts: -------------------------------------------------------------------------------- 1 | import { ClientRequest, IncomingMessage } from "http"; 2 | import { HttpPlugin } from "./plugin.ts"; 3 | 4 | export class StripePlugin extends HttpPlugin implements HttpPlugin { 5 | captureBody = true 6 | name = 'stripe' 7 | 8 | shouldParseRequest(request: ClientRequest | IncomingMessage): boolean { 9 | if (request instanceof ClientRequest && request.host?.includes('api.stripe.com')) { 10 | return true; 11 | } 12 | return false; 13 | } 14 | 15 | parseClientRequest(request: ClientRequest) { 16 | const method = request.method; 17 | 18 | const [version, entity, entityIdOrOperation, operation] = request.path.split('/'); 19 | 20 | return { 21 | stripe: { 22 | version, 23 | method, 24 | entity, 25 | entityIdOrOperation, 26 | operation, 27 | } 28 | } 29 | } 30 | } 31 | 32 | const plugin = new StripePlugin(); -------------------------------------------------------------------------------- /src/http-plugins/vercel.ts: -------------------------------------------------------------------------------- 1 | import { ClientRequest, IncomingMessage } from "http"; 2 | import { HttpPlugin } from "./plugin.ts"; 3 | 4 | export class VercelPlugin extends HttpPlugin implements HttpPlugin { 5 | name = 'vercel'; 6 | shouldParseRequest(request: ClientRequest | IncomingMessage): boolean { 7 | 8 | if (request instanceof IncomingMessage && request.headers['x-vercel-id']) { 9 | return true; 10 | } 11 | return false; 12 | } 13 | parseIncommingMessage(request: IncomingMessage) { 14 | 15 | const headers = request.headers; 16 | const vercelRequestId = headers['x-vercel-id']; 17 | 18 | if (typeof vercelRequestId === "string") { 19 | const requestIdParts = vercelRequestId.split("::"); 20 | const requestId = requestIdParts[requestIdParts.length - 1]; 21 | const user = { 22 | ip: headers['x-forwarded-for'], 23 | country: headers['x-vercel-ip-country'], 24 | region: headers['x-vercel-ip-region'], 25 | city: headers['x-vercel-ip-city'], 26 | latitude: headers['x-vercel-ip-latitude'], 27 | longitude: headers['x-vercel-ip-longitude'], 28 | timezone: headers['x-vercel-ip-timezone'], 29 | } 30 | return { 31 | requestId: requestId, 32 | faas: { execution: requestId }, 33 | user 34 | } 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /src/http.ts: -------------------------------------------------------------------------------- 1 | import { Span } from "@opentelemetry/api"; 2 | import { ClientRequest, IncomingMessage, OutgoingHttpHeaders, ServerResponse } from "http"; 3 | import { HttpPlugin } from "./http-plugins/plugin.ts"; 4 | import { flatten } from "flat"; 5 | import { HttpInstrumentation } from "./http/index.ts"; 6 | import { HttpInstrumentationConfig } from "./http/types.ts" 7 | import { parse } from 'querystring' 8 | import { PassThrough } from "stream"; 9 | 10 | type BetterHttpInstrumentationOptions = { 11 | plugins?: HttpPlugin[], 12 | captureBody?: boolean, 13 | captureHeaders?: boolean, 14 | requestHook?: HttpInstrumentationConfig['requestHook'] 15 | responseHook?: HttpInstrumentationConfig['responseHook'] 16 | ignoreIncomingRequestHook?: HttpInstrumentationConfig['ignoreIncomingRequestHook'] 17 | ignoreOutgoingRequestHook?: HttpInstrumentationConfig['ignoreOutgoingRequestHook'] 18 | startIncomingSpanHook?: HttpInstrumentationConfig['startIncomingSpanHook'] 19 | startOutgoingSpanHook?: HttpInstrumentationConfig['startOutgoingSpanHook'] 20 | } 21 | 22 | export function _betterHttpInstrumentation(options: BetterHttpInstrumentationOptions = {}) { 23 | options.plugins = options.plugins || []; 24 | return { 25 | requestHook(span: Span, request: ClientRequest | IncomingMessage) { 26 | if (request instanceof ClientRequest) { 27 | const plugin = options.plugins.find(plugin => plugin?.shouldParseRequest(request)) as HttpPlugin | undefined; 28 | 29 | if (plugin) { 30 | span.setAttribute('http.plugin.name', plugin.name); 31 | 32 | const headers = request.getHeaders(); 33 | 34 | 35 | if (options.captureHeaders) { 36 | span.setAttributes(flatten({ request: { headers } })); 37 | } 38 | if (plugin.captureBody) { 39 | getClientRequestBody(request, (body) => { 40 | const requestData = _parseBodySafe(body, headers); 41 | span.setAttributes(flatten({ request: { body: requestData } })); 42 | }) 43 | } 44 | } else { 45 | 46 | const headers = request.getHeaders(); 47 | 48 | 49 | if (options.captureHeaders) { 50 | span.setAttributes(flatten({ request: { headers } })); 51 | } 52 | 53 | if (options.captureBody && shouldCaptureBody(request.host)) { 54 | getClientRequestBody(request, (body) => { 55 | const requestData = _parseBodySafe(body, headers); 56 | span.setAttributes(flatten({ request: { body: requestData } })); 57 | }) 58 | 59 | } 60 | } 61 | 62 | } 63 | if (request instanceof IncomingMessage) { 64 | const plugin = options.plugins.find(plugin => plugin.shouldParseRequest && plugin.shouldParseRequest(request)); 65 | 66 | span.setAttribute('http.plugin.name', plugin.name); 67 | 68 | if (plugin.parseIncommingMessage) { 69 | const attributes = plugin.parseIncommingMessage(request); 70 | span.setAttributes(flatten(attributes)); 71 | } 72 | } 73 | 74 | if (options.requestHook) { 75 | options.requestHook(span, request); 76 | } 77 | }, 78 | responseHook(span: Span, response: IncomingMessage | ServerResponse, cb: () => void) { 79 | if (response instanceof IncomingMessage) { 80 | try { 81 | const headers = response.headers; 82 | if (options.captureHeaders) { 83 | span.setAttributes(flatten({ response: { headers } })); 84 | } 85 | 86 | 87 | if (options.captureBody && shouldCaptureBody(response.url || '')) { 88 | getClientResponseBody(response, (body) => { 89 | const responseData = _parseBodySafe(body, headers); 90 | span.setAttributes(flatten({ response: { body: responseData } })); 91 | cb(); 92 | }) 93 | } else { 94 | cb(); 95 | } 96 | } catch (e) { 97 | cb(); 98 | } 99 | } 100 | 101 | if (options.responseHook) { 102 | options.responseHook(span, response, cb); 103 | } 104 | 105 | }, 106 | } 107 | } 108 | 109 | const ignoredHosts = [ 110 | 'localhost', 111 | 'otel.baselime', 112 | ]; 113 | 114 | function getClientRequestBody(r: ClientRequest, cb: (body: string) => void) { 115 | const chunks: Buffer[] = []; 116 | const oldWrite = r.write.bind(r); 117 | r.write = (data: Buffer | string) => { 118 | try { 119 | if (typeof data === 'string') { 120 | chunks.push(Buffer.from(data)); 121 | 122 | if (data[data.length - 1] === '}') { 123 | const body = Buffer.concat(chunks).toString('utf8'); 124 | cb(body); 125 | } 126 | } else { 127 | chunks.push(data); 128 | 129 | if (data[data.length - 1] === 125) { 130 | const body = Buffer.concat(chunks).toString('utf8'); 131 | cb(body); 132 | } 133 | } 134 | } catch (e) { 135 | } 136 | return oldWrite(data); 137 | }; 138 | const oldEnd = r.end.bind(r); 139 | r.end = (data: any) => { 140 | try { 141 | if (data) { 142 | if (typeof data === 'string') { 143 | chunks.push(Buffer.from(data)); 144 | } else { 145 | chunks.push(data); 146 | } 147 | } 148 | if (chunks.length > 0) { 149 | const body = Buffer.concat(chunks).toString('utf8'); 150 | cb(body); 151 | } 152 | } catch (e) { 153 | } 154 | return oldEnd(data); 155 | }; 156 | }; 157 | 158 | function getClientResponseBody(r: IncomingMessage, cb: (body: string) => void) { 159 | const chunks: Buffer[] = []; 160 | const pt = new PassThrough(); 161 | 162 | pt.on('data', (chunk) => { 163 | try { 164 | if (typeof chunk === 'string') { 165 | chunks.push(Buffer.from(chunk)); 166 | } else { 167 | chunks.push(chunk); 168 | } 169 | } catch (e) { 170 | } 171 | }).on('end', () => { 172 | try { 173 | if (chunks.length > 0) { 174 | const body = Buffer.concat(chunks).toString('utf8'); 175 | cb(body) 176 | } 177 | } catch (e) { 178 | } 179 | }); 180 | 181 | const originalState = r.readableFlowing; 182 | r.pipe(pt); 183 | // @ts-ignore 184 | r.readableFlowing = originalState; 185 | } 186 | 187 | function shouldCaptureBody(host: string) { 188 | return !ignoredHosts.find(ignoredHost => host.includes(ignoredHost)); 189 | } 190 | 191 | function _parseBodySafe(body: string, headers: OutgoingHttpHeaders): unknown { 192 | let requestData: unknown = body; 193 | try { 194 | if (headers['content-type'] && typeof headers['content-type'] === 'string') { 195 | if (headers['content-type'].includes('application/json') || headers['content-type'].includes('application/x-amz-json')) { 196 | requestData = JSON.parse(body); 197 | } else if (headers['content-type'].includes('application/x-www-form-urlencoded')) { 198 | requestData = parse(body); 199 | } 200 | } 201 | } catch (_) { 202 | } 203 | 204 | return requestData; 205 | } 206 | 207 | export class BetterHttpInstrumentation extends HttpInstrumentation { 208 | constructor(options: BetterHttpInstrumentationOptions = {}) { 209 | super({ 210 | ..._betterHttpInstrumentation(options), 211 | ignoreIncomingRequestHook: options.ignoreIncomingRequestHook, 212 | ignoreOutgoingRequestHook: options.ignoreOutgoingRequestHook, 213 | startIncomingSpanHook: options.startIncomingSpanHook, 214 | startOutgoingSpanHook: options.startOutgoingSpanHook, 215 | }) 216 | } 217 | } -------------------------------------------------------------------------------- /src/http/enums/AttributeNames.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright The OpenTelemetry Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/semantic_conventions/http.md 19 | */ 20 | export enum AttributeNames { 21 | HTTP_ERROR_NAME = 'http.error_name', 22 | HTTP_ERROR_MESSAGE = 'http.error_message', 23 | HTTP_STATUS_TEXT = 'http.status_text', 24 | } -------------------------------------------------------------------------------- /src/http/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright The OpenTelemetry Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { 17 | context, 18 | HrTime, 19 | INVALID_SPAN_CONTEXT, 20 | propagation, 21 | ROOT_CONTEXT, 22 | Span, 23 | SpanKind, 24 | SpanOptions, 25 | SpanStatus, 26 | SpanStatusCode, 27 | trace, 28 | Histogram, 29 | MetricAttributes, 30 | ValueType, 31 | } from '@opentelemetry/api'; 32 | import { 33 | hrTime, 34 | hrTimeDuration, 35 | hrTimeToMilliseconds, 36 | suppressTracing, 37 | } from '@opentelemetry/core'; 38 | import type * as http from 'http'; 39 | import type * as https from 'https'; 40 | import { Socket } from 'net'; 41 | import * as url from 'url'; 42 | import { 43 | Err, 44 | Func, 45 | Http, 46 | HttpInstrumentationConfig, 47 | HttpRequestArgs, 48 | Https, 49 | } from './types.ts'; 50 | import * as utils from './utils.ts'; 51 | import { 52 | InstrumentationBase, 53 | InstrumentationNodeModuleDefinition, 54 | isWrapped, 55 | safeExecuteInTheMiddle, 56 | } from '@opentelemetry/instrumentation'; 57 | import { RPCMetadata, RPCType, setRPCMetadata } from '@opentelemetry/core'; 58 | import { errorMonitor } from 'events'; 59 | import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 60 | 61 | /** 62 | * Http instrumentation instrumentation for Opentelemetry 63 | */ 64 | export class HttpInstrumentation extends InstrumentationBase { 65 | /** keep track on spans not ended */ 66 | private readonly _spanNotEnded: WeakSet = new WeakSet(); 67 | private _headerCapture; 68 | private _httpServerDurationHistogram!: Histogram; 69 | private _httpClientDurationHistogram!: Histogram; 70 | 71 | constructor(config?: HttpInstrumentationConfig) { 72 | super('@opentelemetry/instrumentation-http', '1.0', config); 73 | this._headerCapture = this._createHeaderCapture(); 74 | } 75 | 76 | protected override _updateMetricInstruments() { 77 | this._httpServerDurationHistogram = this.meter.createHistogram( 78 | 'http.server.duration', 79 | { 80 | description: 'Measures the duration of inbound HTTP requests.', 81 | unit: 'ms', 82 | valueType: ValueType.DOUBLE, 83 | } 84 | ); 85 | this._httpClientDurationHistogram = this.meter.createHistogram( 86 | 'http.client.duration', 87 | { 88 | description: 'Measures the duration of outbound HTTP requests.', 89 | unit: 'ms', 90 | valueType: ValueType.DOUBLE, 91 | } 92 | ); 93 | } 94 | 95 | private _getConfig(): HttpInstrumentationConfig { 96 | return this._config; 97 | } 98 | 99 | override setConfig(config?: HttpInstrumentationConfig): void { 100 | super.setConfig(config); 101 | this._headerCapture = this._createHeaderCapture(); 102 | } 103 | 104 | init(): [ 105 | InstrumentationNodeModuleDefinition, 106 | InstrumentationNodeModuleDefinition, 107 | ] { 108 | return [this._getHttpsInstrumentation(), this._getHttpInstrumentation()]; 109 | } 110 | 111 | private _getHttpInstrumentation() { 112 | const version = process.versions.node; 113 | return new InstrumentationNodeModuleDefinition( 114 | 'http', 115 | ['*'], 116 | moduleExports => { 117 | this._diag.debug(`Applying patch for http@${version}`); 118 | if (isWrapped(moduleExports.request)) { 119 | this._unwrap(moduleExports, 'request'); 120 | } 121 | this._wrap( 122 | moduleExports, 123 | 'request', 124 | this._getPatchOutgoingRequestFunction('http') 125 | ); 126 | if (isWrapped(moduleExports.get)) { 127 | this._unwrap(moduleExports, 'get'); 128 | } 129 | this._wrap( 130 | moduleExports, 131 | 'get', 132 | this._getPatchOutgoingGetFunction(moduleExports.request) 133 | ); 134 | if (isWrapped(moduleExports.Server.prototype.emit)) { 135 | this._unwrap(moduleExports.Server.prototype, 'emit'); 136 | } 137 | this._wrap( 138 | moduleExports.Server.prototype, 139 | 'emit', 140 | this._getPatchIncomingRequestFunction('http') 141 | ); 142 | return moduleExports; 143 | }, 144 | moduleExports => { 145 | if (moduleExports === undefined) return; 146 | this._diag.debug(`Removing patch for http@${version}`); 147 | 148 | this._unwrap(moduleExports, 'request'); 149 | this._unwrap(moduleExports, 'get'); 150 | this._unwrap(moduleExports.Server.prototype, 'emit'); 151 | } 152 | ); 153 | } 154 | 155 | private _getHttpsInstrumentation() { 156 | const version = process.versions.node; 157 | return new InstrumentationNodeModuleDefinition( 158 | 'https', 159 | ['*'], 160 | moduleExports => { 161 | this._diag.debug(`Applying patch for https@${version}`); 162 | if (isWrapped(moduleExports.request)) { 163 | this._unwrap(moduleExports, 'request'); 164 | } 165 | this._wrap( 166 | moduleExports, 167 | 'request', 168 | this._getPatchHttpsOutgoingRequestFunction('https') 169 | ); 170 | if (isWrapped(moduleExports.get)) { 171 | this._unwrap(moduleExports, 'get'); 172 | } 173 | this._wrap( 174 | moduleExports, 175 | 'get', 176 | this._getPatchHttpsOutgoingGetFunction(moduleExports.request) 177 | ); 178 | if (isWrapped(moduleExports.Server.prototype.emit)) { 179 | this._unwrap(moduleExports.Server.prototype, 'emit'); 180 | } 181 | this._wrap( 182 | moduleExports.Server.prototype, 183 | 'emit', 184 | this._getPatchIncomingRequestFunction('https') 185 | ); 186 | return moduleExports; 187 | }, 188 | moduleExports => { 189 | if (moduleExports === undefined) return; 190 | this._diag.debug(`Removing patch for https@${version}`); 191 | 192 | this._unwrap(moduleExports, 'request'); 193 | this._unwrap(moduleExports, 'get'); 194 | this._unwrap(moduleExports.Server.prototype, 'emit'); 195 | } 196 | ); 197 | } 198 | 199 | /** 200 | * Creates spans for incoming requests, restoring spans' context if applied. 201 | */ 202 | protected _getPatchIncomingRequestFunction(component: 'http' | 'https') { 203 | return ( 204 | original: (event: string, ...args: unknown[]) => boolean 205 | ): ((this: unknown, event: string, ...args: unknown[]) => boolean) => { 206 | return this._incomingRequestFunction(component, original); 207 | }; 208 | } 209 | 210 | /** 211 | * Creates spans for outgoing requests, sending spans' context for distributed 212 | * tracing. 213 | */ 214 | protected _getPatchOutgoingRequestFunction(component: 'http' | 'https') { 215 | return (original: Func): Func => { 216 | return this._outgoingRequestFunction(component, original); 217 | }; 218 | } 219 | 220 | protected _getPatchOutgoingGetFunction( 221 | clientRequest: ( 222 | options: http.RequestOptions | string | url.URL, 223 | ...args: HttpRequestArgs 224 | ) => http.ClientRequest 225 | ) { 226 | return (_original: Func): Func => { 227 | // Re-implement http.get. This needs to be done (instead of using 228 | // getPatchOutgoingRequestFunction to patch it) because we need to 229 | // set the trace context header before the returned http.ClientRequest is 230 | // ended. The Node.js docs state that the only differences between 231 | // request and get are that (1) get defaults to the HTTP GET method and 232 | // (2) the returned request object is ended immediately. The former is 233 | // already true (at least in supported Node versions up to v10), so we 234 | // simply follow the latter. Ref: 235 | // https://nodejs.org/dist/latest/docs/api/http.html#http_http_get_options_callback 236 | // https://github.com/googleapis/cloud-trace-nodejs/blob/master/src/instrumentations/instrumentation-http.ts#L198 237 | return function outgoingGetRequest< 238 | T extends http.RequestOptions | string | url.URL, 239 | >(options: T, ...args: HttpRequestArgs): http.ClientRequest { 240 | const req = clientRequest(options, ...args); 241 | req.end(); 242 | return req; 243 | }; 244 | }; 245 | } 246 | 247 | /** Patches HTTPS outgoing requests */ 248 | private _getPatchHttpsOutgoingRequestFunction(component: 'http' | 'https') { 249 | return (original: Func): Func => { 250 | const instrumentation = this; 251 | return function httpsOutgoingRequest( 252 | // eslint-disable-next-line node/no-unsupported-features/node-builtins 253 | options: https.RequestOptions | string | URL, 254 | ...args: HttpRequestArgs 255 | ): http.ClientRequest { 256 | // Makes sure options will have default HTTPS parameters 257 | if ( 258 | component === 'https' && 259 | typeof options === 'object' && 260 | options?.constructor?.name !== 'URL' 261 | ) { 262 | options = Object.assign({}, options); 263 | instrumentation._setDefaultOptions(options); 264 | } 265 | return instrumentation._getPatchOutgoingRequestFunction(component)( 266 | original 267 | )(options, ...args); 268 | }; 269 | }; 270 | } 271 | 272 | private _setDefaultOptions(options: https.RequestOptions) { 273 | options.protocol = options.protocol || 'https:'; 274 | options.port = options.port || 443; 275 | } 276 | 277 | /** Patches HTTPS outgoing get requests */ 278 | private _getPatchHttpsOutgoingGetFunction( 279 | clientRequest: ( 280 | // eslint-disable-next-line node/no-unsupported-features/node-builtins 281 | options: http.RequestOptions | string | URL, 282 | ...args: HttpRequestArgs 283 | ) => http.ClientRequest 284 | ) { 285 | return (original: Func): Func => { 286 | const instrumentation = this; 287 | return function httpsOutgoingRequest( 288 | // eslint-disable-next-line node/no-unsupported-features/node-builtins 289 | options: https.RequestOptions | string | URL, 290 | ...args: HttpRequestArgs 291 | ): http.ClientRequest { 292 | return instrumentation._getPatchOutgoingGetFunction(clientRequest)( 293 | original 294 | )(options, ...args); 295 | }; 296 | }; 297 | } 298 | 299 | /** 300 | * Attach event listeners to a client request to end span and add span attributes. 301 | * 302 | * @param request The original request object. 303 | * @param span representing the current operation 304 | * @param startTime representing the start time of the request to calculate duration in Metric 305 | * @param metricAttributes metric attributes 306 | */ 307 | private _traceClientRequest( 308 | request: http.ClientRequest, 309 | span: Span, 310 | startTime: HrTime, 311 | metricAttributes: MetricAttributes 312 | ): http.ClientRequest { 313 | if (this._getConfig().requestHook) { 314 | this._callRequestHook(span, request); 315 | } 316 | 317 | /** 318 | * Determines if the request has errored or the response has ended/errored. 319 | */ 320 | let responseFinished = false; 321 | 322 | /* 323 | * User 'response' event listeners can be added before our listener, 324 | * force our listener to be the first, so response emitter is bound 325 | * before any user listeners are added to it. 326 | */ 327 | request.prependListener( 328 | 'response', 329 | (response: http.IncomingMessage & { aborted?: boolean }) => { 330 | this._diag.debug('outgoingRequest on response()'); 331 | if (request.listenerCount('response') <= 1) { 332 | response.resume(); 333 | } 334 | const responseAttributes = 335 | utils.getOutgoingRequestAttributesOnResponse(response); 336 | span.setAttributes(responseAttributes); 337 | metricAttributes = Object.assign( 338 | metricAttributes, 339 | utils.getOutgoingRequestMetricAttributesOnResponse(responseAttributes) 340 | ); 341 | 342 | if (this._getConfig().responseHook) { 343 | this._callResponseHook(span, response, this, startTime, metricAttributes); 344 | } 345 | 346 | this._headerCapture.client.captureRequestHeaders(span, header => 347 | request.getHeader(header) 348 | ); 349 | this._headerCapture.client.captureResponseHeaders( 350 | span, 351 | header => response.headers[header] 352 | ); 353 | 354 | context.bind(context.active(), response); 355 | 356 | const endHandler = () => { 357 | this._diag.debug('outgoingRequest on end()'); 358 | if (responseFinished) { 359 | return; 360 | } 361 | responseFinished = true; 362 | let status: SpanStatus; 363 | 364 | if (response.aborted && !response.complete) { 365 | status = { code: SpanStatusCode.ERROR }; 366 | } else { 367 | status = { 368 | code: utils.parseResponseStatus( 369 | SpanKind.CLIENT, 370 | response.statusCode 371 | ), 372 | }; 373 | } 374 | 375 | span.setStatus(status); 376 | 377 | if (this._getConfig().applyCustomAttributesOnSpan) { 378 | safeExecuteInTheMiddle( 379 | () => 380 | this._getConfig().applyCustomAttributesOnSpan!( 381 | span, 382 | request, 383 | response 384 | ), 385 | () => { }, 386 | true 387 | ); 388 | } 389 | 390 | if (!this._getConfig().responseHook) { 391 | this._closeHttpSpan( 392 | span, 393 | SpanKind.CLIENT, 394 | startTime, 395 | metricAttributes 396 | ); 397 | } 398 | }; 399 | 400 | response.on('end', endHandler); 401 | response.on(errorMonitor, (error: Err) => { 402 | this._diag.debug('outgoingRequest on error()', error); 403 | if (responseFinished) { 404 | return; 405 | } 406 | responseFinished = true; 407 | utils.setSpanWithError(span, error); 408 | span.setStatus({ 409 | code: SpanStatusCode.ERROR, 410 | message: error.message, 411 | }); 412 | this._closeHttpSpan( 413 | span, 414 | SpanKind.CLIENT, 415 | startTime, 416 | metricAttributes 417 | ); 418 | }); 419 | } 420 | ); 421 | request.on('close', () => { 422 | this._diag.debug('outgoingRequest on request close()'); 423 | if (request.aborted || responseFinished) { 424 | return; 425 | } 426 | responseFinished = true; 427 | this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes); 428 | }); 429 | request.on(errorMonitor, (error: Err) => { 430 | this._diag.debug('outgoingRequest on request error()', error); 431 | if (responseFinished) { 432 | return; 433 | } 434 | responseFinished = true; 435 | utils.setSpanWithError(span, error); 436 | this._closeHttpSpan(span, SpanKind.CLIENT, startTime, metricAttributes); 437 | }); 438 | 439 | this._diag.debug('http.ClientRequest return request'); 440 | return request; 441 | } 442 | 443 | private _incomingRequestFunction( 444 | component: 'http' | 'https', 445 | original: (event: string, ...args: unknown[]) => boolean 446 | ) { 447 | const instrumentation = this; 448 | return function incomingRequest( 449 | this: unknown, 450 | event: string, 451 | ...args: unknown[] 452 | ): boolean { 453 | // Only traces request events 454 | if (event !== 'request') { 455 | return original.apply(this, [event, ...args]); 456 | } 457 | 458 | const request = args[0] as http.IncomingMessage; 459 | const response = args[1] as http.ServerResponse & { socket: Socket }; 460 | const pathname = request.url 461 | ? url.parse(request.url).pathname || '/' 462 | : '/'; 463 | const method = request.method || 'GET'; 464 | 465 | instrumentation._diag.debug( 466 | `${component} instrumentation incomingRequest` 467 | ); 468 | 469 | if ( 470 | utils.isIgnored( 471 | pathname, 472 | instrumentation._getConfig().ignoreIncomingPaths, 473 | (e: unknown) => 474 | instrumentation._diag.error('caught ignoreIncomingPaths error: ', e) 475 | ) || 476 | safeExecuteInTheMiddle( 477 | () => 478 | instrumentation._getConfig().ignoreIncomingRequestHook?.(request), 479 | (e: unknown) => { 480 | if (e != null) { 481 | instrumentation._diag.error( 482 | 'caught ignoreIncomingRequestHook error: ', 483 | e 484 | ); 485 | } 486 | }, 487 | true 488 | ) 489 | ) { 490 | return context.with(suppressTracing(context.active()), () => { 491 | context.bind(context.active(), request); 492 | context.bind(context.active(), response); 493 | return original.apply(this, [event, ...args]); 494 | }); 495 | } 496 | 497 | const headers = request.headers; 498 | 499 | const spanAttributes = utils.getIncomingRequestAttributes(request, { 500 | component: component, 501 | serverName: instrumentation._getConfig().serverName, 502 | hookAttributes: instrumentation._callStartSpanHook( 503 | request, 504 | instrumentation._getConfig().startIncomingSpanHook 505 | ), 506 | }); 507 | 508 | const spanOptions: SpanOptions = { 509 | kind: SpanKind.SERVER, 510 | attributes: spanAttributes, 511 | }; 512 | 513 | const startTime = hrTime(); 514 | const metricAttributes = 515 | utils.getIncomingRequestMetricAttributes(spanAttributes); 516 | 517 | const ctx = propagation.extract(ROOT_CONTEXT, headers); 518 | const span = instrumentation._startHttpSpan(method, spanOptions, ctx); 519 | const rpcMetadata: RPCMetadata = { 520 | type: RPCType.HTTP, 521 | span, 522 | }; 523 | 524 | return context.with( 525 | setRPCMetadata(trace.setSpan(ctx, span), rpcMetadata), 526 | () => { 527 | context.bind(context.active(), request); 528 | context.bind(context.active(), response); 529 | 530 | if (instrumentation._getConfig().requestHook) { 531 | instrumentation._callRequestHook(span, request); 532 | } 533 | if (instrumentation._getConfig().responseHook) { 534 | instrumentation._callResponseHook(span, response, instrumentation, startTime, metricAttributes); 535 | } 536 | 537 | instrumentation._headerCapture.server.captureRequestHeaders( 538 | span, 539 | (header: string) => request.headers[header] 540 | ); 541 | 542 | // After 'error', no further events other than 'close' should be emitted. 543 | let hasError = false; 544 | response.on('close', () => { 545 | if (hasError) { 546 | return; 547 | } 548 | instrumentation._onServerResponseFinish( 549 | request, 550 | response, 551 | span, 552 | metricAttributes, 553 | startTime 554 | ); 555 | }); 556 | response.on(errorMonitor, (err: Err) => { 557 | hasError = true; 558 | instrumentation._onServerResponseError( 559 | span, 560 | metricAttributes, 561 | startTime, 562 | err 563 | ); 564 | }); 565 | 566 | return safeExecuteInTheMiddle( 567 | () => original.apply(this, [event, ...args]), 568 | error => { 569 | if (error) { 570 | utils.setSpanWithError(span, error); 571 | instrumentation._closeHttpSpan( 572 | span, 573 | SpanKind.SERVER, 574 | startTime, 575 | metricAttributes 576 | ); 577 | throw error; 578 | } 579 | } 580 | ); 581 | } 582 | ); 583 | }; 584 | } 585 | 586 | private _outgoingRequestFunction( 587 | component: 'http' | 'https', 588 | original: Func 589 | ): Func { 590 | const instrumentation = this; 591 | return function outgoingRequest( 592 | this: unknown, 593 | options: url.URL | http.RequestOptions | string, 594 | ...args: unknown[] 595 | ): http.ClientRequest { 596 | if (!utils.isValidOptionsType(options)) { 597 | return original.apply(this, [options, ...args]); 598 | } 599 | const extraOptions = 600 | typeof args[0] === 'object' && 601 | (typeof options === 'string' || options instanceof url.URL) 602 | ? (args.shift() as http.RequestOptions) 603 | : undefined; 604 | const { origin, pathname, method, optionsParsed } = utils.getRequestInfo( 605 | options, 606 | extraOptions 607 | ); 608 | 609 | if ( 610 | utils.isIgnored( 611 | origin + pathname, 612 | instrumentation._getConfig().ignoreOutgoingUrls, 613 | (e: unknown) => 614 | instrumentation._diag.error('caught ignoreOutgoingUrls error: ', e) 615 | ) || 616 | safeExecuteInTheMiddle( 617 | () => 618 | instrumentation 619 | ._getConfig() 620 | .ignoreOutgoingRequestHook?.(optionsParsed), 621 | (e: unknown) => { 622 | if (e != null) { 623 | instrumentation._diag.error( 624 | 'caught ignoreOutgoingRequestHook error: ', 625 | e 626 | ); 627 | } 628 | }, 629 | true 630 | ) 631 | ) { 632 | return original.apply(this, [optionsParsed, ...args]); 633 | } 634 | 635 | const { hostname, port } = utils.extractHostnameAndPort(optionsParsed); 636 | 637 | const attributes = utils.getOutgoingRequestAttributes(optionsParsed, { 638 | component, 639 | port, 640 | hostname, 641 | hookAttributes: instrumentation._callStartSpanHook( 642 | optionsParsed, 643 | instrumentation._getConfig().startOutgoingSpanHook 644 | ), 645 | }); 646 | 647 | const startTime = hrTime(); 648 | const metricAttributes: MetricAttributes = 649 | utils.getOutgoingRequestMetricAttributes(attributes); 650 | 651 | const spanOptions: SpanOptions = { 652 | kind: SpanKind.CLIENT, 653 | attributes, 654 | }; 655 | const span = instrumentation._startHttpSpan(method, spanOptions); 656 | 657 | const parentContext = context.active(); 658 | const requestContext = trace.setSpan(parentContext, span); 659 | 660 | if (!optionsParsed.headers) { 661 | optionsParsed.headers = {}; 662 | } else { 663 | // Make a copy of the headers object to avoid mutating an object the 664 | // caller might have a reference to. 665 | optionsParsed.headers = Object.assign({}, optionsParsed.headers); 666 | } 667 | propagation.inject(requestContext, optionsParsed.headers); 668 | 669 | return context.with(requestContext, () => { 670 | /* 671 | * The response callback is registered before ClientRequest is bound, 672 | * thus it is needed to bind it before the function call. 673 | */ 674 | const cb = args[args.length - 1]; 675 | if (typeof cb === 'function') { 676 | args[args.length - 1] = context.bind(parentContext, cb); 677 | } 678 | 679 | const request: http.ClientRequest = safeExecuteInTheMiddle( 680 | () => original.apply(this, [optionsParsed, ...args]), 681 | error => { 682 | if (error) { 683 | utils.setSpanWithError(span, error); 684 | instrumentation._closeHttpSpan( 685 | span, 686 | SpanKind.CLIENT, 687 | startTime, 688 | metricAttributes 689 | ); 690 | throw error; 691 | } 692 | } 693 | ); 694 | 695 | instrumentation._diag.debug( 696 | `${component} instrumentation outgoingRequest` 697 | ); 698 | context.bind(parentContext, request); 699 | return instrumentation._traceClientRequest( 700 | request, 701 | span, 702 | startTime, 703 | metricAttributes 704 | ); 705 | }); 706 | }; 707 | } 708 | 709 | private _onServerResponseFinish( 710 | request: http.IncomingMessage, 711 | response: http.ServerResponse, 712 | span: Span, 713 | metricAttributes: MetricAttributes, 714 | startTime: HrTime 715 | ) { 716 | const attributes = utils.getIncomingRequestAttributesOnResponse( 717 | request, 718 | response 719 | ); 720 | metricAttributes = Object.assign( 721 | metricAttributes, 722 | utils.getIncomingRequestMetricAttributesOnResponse(attributes) 723 | ); 724 | 725 | this._headerCapture.server.captureResponseHeaders(span, header => 726 | response.getHeader(header) 727 | ); 728 | 729 | span.setAttributes(attributes).setStatus({ 730 | code: utils.parseResponseStatus(SpanKind.SERVER, response.statusCode), 731 | }); 732 | 733 | const route = attributes[SemanticAttributes.HTTP_ROUTE]; 734 | if (route) { 735 | span.updateName(`${request.method || 'GET'} ${route}`); 736 | } 737 | 738 | if (this._getConfig().applyCustomAttributesOnSpan) { 739 | safeExecuteInTheMiddle( 740 | () => 741 | this._getConfig().applyCustomAttributesOnSpan!( 742 | span, 743 | request, 744 | response 745 | ), 746 | () => { }, 747 | true 748 | ); 749 | } 750 | 751 | this._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes); 752 | } 753 | 754 | private _onServerResponseError( 755 | span: Span, 756 | metricAttributes: MetricAttributes, 757 | startTime: HrTime, 758 | error: Err 759 | ) { 760 | utils.setSpanWithError(span, error); 761 | this._closeHttpSpan(span, SpanKind.SERVER, startTime, metricAttributes); 762 | } 763 | 764 | private _startHttpSpan( 765 | name: string, 766 | options: SpanOptions, 767 | ctx = context.active() 768 | ) { 769 | /* 770 | * If a parent is required but not present, we use a `NoopSpan` to still 771 | * propagate context without recording it. 772 | */ 773 | const requireParent = 774 | options.kind === SpanKind.CLIENT 775 | ? this._getConfig().requireParentforOutgoingSpans 776 | : this._getConfig().requireParentforIncomingSpans; 777 | 778 | let span: Span; 779 | const currentSpan = trace.getSpan(ctx); 780 | 781 | if (requireParent === true && currentSpan === undefined) { 782 | span = trace.wrapSpanContext(INVALID_SPAN_CONTEXT); 783 | } else if (requireParent === true && currentSpan?.spanContext().isRemote) { 784 | span = currentSpan; 785 | } else { 786 | span = this.tracer.startSpan(name, options, ctx); 787 | } 788 | this._spanNotEnded.add(span); 789 | return span; 790 | } 791 | 792 | private _closeHttpSpan( 793 | span: Span, 794 | spanKind: SpanKind, 795 | startTime: HrTime, 796 | metricAttributes: MetricAttributes 797 | ) { 798 | if (!this._spanNotEnded.has(span)) { 799 | return; 800 | } 801 | 802 | span.end(); 803 | this._spanNotEnded.delete(span); 804 | 805 | // Record metrics 806 | const duration = hrTimeToMilliseconds(hrTimeDuration(startTime, hrTime())); 807 | if (spanKind === SpanKind.SERVER) { 808 | this._httpServerDurationHistogram.record(duration, metricAttributes); 809 | } else if (spanKind === SpanKind.CLIENT) { 810 | this._httpClientDurationHistogram.record(duration, metricAttributes); 811 | } 812 | } 813 | 814 | private _callResponseHook( 815 | span: Span, 816 | response: http.IncomingMessage | http.ServerResponse, 817 | instrumentation: HttpInstrumentation, 818 | startTime: HrTime, 819 | metricAttributes: MetricAttributes 820 | ) { 821 | safeExecuteInTheMiddle( 822 | () => this._getConfig().responseHook!(span, response, () => { 823 | instrumentation._closeHttpSpan( 824 | span, 825 | SpanKind.SERVER, 826 | startTime, 827 | metricAttributes 828 | ); 829 | }), 830 | () => { }, 831 | true 832 | ); 833 | } 834 | 835 | private _callRequestHook( 836 | span: Span, 837 | request: http.ClientRequest | http.IncomingMessage 838 | ) { 839 | safeExecuteInTheMiddle( 840 | () => this._getConfig().requestHook!(span, request), 841 | () => { }, 842 | true 843 | ); 844 | } 845 | 846 | private _callStartSpanHook( 847 | request: http.IncomingMessage | http.RequestOptions, 848 | hookFunc: Function | undefined 849 | ) { 850 | if (typeof hookFunc === 'function') { 851 | return safeExecuteInTheMiddle( 852 | () => hookFunc(request), 853 | () => { }, 854 | true 855 | ); 856 | } 857 | } 858 | 859 | private _createHeaderCapture() { 860 | const config = this._getConfig(); 861 | 862 | return { 863 | client: { 864 | captureRequestHeaders: utils.headerCapture( 865 | 'request', 866 | config.headersToSpanAttributes?.client?.requestHeaders ?? [] 867 | ), 868 | captureResponseHeaders: utils.headerCapture( 869 | 'response', 870 | config.headersToSpanAttributes?.client?.responseHeaders ?? [] 871 | ), 872 | }, 873 | server: { 874 | captureRequestHeaders: utils.headerCapture( 875 | 'request', 876 | config.headersToSpanAttributes?.server?.requestHeaders ?? [] 877 | ), 878 | captureResponseHeaders: utils.headerCapture( 879 | 'response', 880 | config.headersToSpanAttributes?.server?.responseHeaders ?? [] 881 | ), 882 | }, 883 | }; 884 | } 885 | } -------------------------------------------------------------------------------- /src/http/readme.md: -------------------------------------------------------------------------------- 1 | # OpenTelemetry HTTP and HTTPS Instrumentation for Node.js 2 | 3 | This is a patched copy of the [@opentelemetry/instrumentation-http](https://github.com/open-telemetry/opentelemetry-js/tree/3920b158d08daa776280bde68a79e44bafa4e8ea/experimental/packages/opentelemetry-instrumentation-http) package. -------------------------------------------------------------------------------- /src/http/types.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright The OpenTelemetry Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { Span, SpanAttributes } from '@opentelemetry/api'; 17 | import type * as http from 'http'; 18 | import type * as https from 'https'; 19 | import { 20 | ClientRequest, 21 | get, 22 | IncomingMessage, 23 | request, 24 | ServerResponse, 25 | RequestOptions, 26 | } from 'http'; 27 | import * as url from 'url'; 28 | import { InstrumentationConfig } from '@opentelemetry/instrumentation'; 29 | 30 | export type IgnoreMatcher = string | RegExp | ((url: string) => boolean); 31 | export type HttpCallback = (res: IncomingMessage) => void; 32 | export type RequestFunction = typeof request; 33 | export type GetFunction = typeof get; 34 | 35 | export type HttpCallbackOptional = HttpCallback | undefined; 36 | 37 | // from node 10+ 38 | export type RequestSignature = [http.RequestOptions, HttpCallbackOptional] & 39 | HttpCallback; 40 | 41 | export type HttpRequestArgs = Array; 42 | 43 | export type ParsedRequestOptions = 44 | | (http.RequestOptions & Partial) 45 | | http.RequestOptions; 46 | export type Http = typeof http; 47 | export type Https = typeof https; 48 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 49 | export type Func = (...args: any[]) => T; 50 | 51 | export interface HttpCustomAttributeFunction { 52 | ( 53 | span: Span, 54 | request: ClientRequest | IncomingMessage, 55 | response: IncomingMessage | ServerResponse 56 | ): void; 57 | } 58 | 59 | export interface IgnoreIncomingRequestFunction { 60 | (request: IncomingMessage): boolean; 61 | } 62 | 63 | export interface IgnoreOutgoingRequestFunction { 64 | (request: RequestOptions): boolean; 65 | } 66 | 67 | export interface HttpRequestCustomAttributeFunction { 68 | (span: Span, request: ClientRequest | IncomingMessage): void; 69 | } 70 | 71 | export interface HttpResponseCustomAttributeFunction { 72 | (span: Span, response: IncomingMessage | ServerResponse, cb: () => void): void; 73 | } 74 | 75 | export interface StartIncomingSpanCustomAttributeFunction { 76 | (request: IncomingMessage): SpanAttributes; 77 | } 78 | 79 | export interface StartOutgoingSpanCustomAttributeFunction { 80 | (request: RequestOptions): SpanAttributes; 81 | } 82 | 83 | /** 84 | * Options available for the HTTP instrumentation (see [documentation](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-instrumentation-http#http-instrumentation-options)) 85 | */ 86 | export interface HttpInstrumentationConfig extends InstrumentationConfig { 87 | /** 88 | * Not trace all incoming requests that match paths 89 | * @deprecated use `ignoreIncomingRequestHook` instead 90 | */ 91 | ignoreIncomingPaths?: IgnoreMatcher[]; 92 | /** Not trace all incoming requests that matched with custom function */ 93 | ignoreIncomingRequestHook?: IgnoreIncomingRequestFunction; 94 | /** 95 | * Not trace all outgoing requests that match urls 96 | * @deprecated use `ignoreOutgoingRequestHook` instead 97 | */ 98 | ignoreOutgoingUrls?: IgnoreMatcher[]; 99 | /** Not trace all outgoing requests that matched with custom function */ 100 | ignoreOutgoingRequestHook?: IgnoreOutgoingRequestFunction; 101 | /** Function for adding custom attributes after response is handled */ 102 | applyCustomAttributesOnSpan?: HttpCustomAttributeFunction; 103 | /** Function for adding custom attributes before request is handled */ 104 | requestHook?: HttpRequestCustomAttributeFunction; 105 | /** Function for adding custom attributes before response is handled */ 106 | responseHook?: HttpResponseCustomAttributeFunction; 107 | /** Function for adding custom attributes before a span is started in incomingRequest */ 108 | startIncomingSpanHook?: StartIncomingSpanCustomAttributeFunction; 109 | /** Function for adding custom attributes before a span is started in outgoingRequest */ 110 | startOutgoingSpanHook?: StartOutgoingSpanCustomAttributeFunction; 111 | /** The primary server name of the matched virtual host. */ 112 | serverName?: string; 113 | /** Require parent to create span for outgoing requests */ 114 | requireParentforOutgoingSpans?: boolean; 115 | /** Require parent to create span for incoming requests */ 116 | requireParentforIncomingSpans?: boolean; 117 | /** Map the following HTTP headers to span attributes. */ 118 | headersToSpanAttributes?: { 119 | client?: { requestHeaders?: string[]; responseHeaders?: string[] }; 120 | server?: { requestHeaders?: string[]; responseHeaders?: string[] }; 121 | }; 122 | } 123 | 124 | export interface Err extends Error { 125 | errno?: number; 126 | code?: string; 127 | path?: string; 128 | syscall?: string; 129 | stack?: string; 130 | } -------------------------------------------------------------------------------- /src/http/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright The OpenTelemetry Authors 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import { 17 | MetricAttributes, 18 | SpanAttributes, 19 | SpanStatusCode, 20 | Span, 21 | context, 22 | SpanKind, 23 | } from '@opentelemetry/api'; 24 | import { 25 | NetTransportValues, 26 | SemanticAttributes, 27 | } from '@opentelemetry/semantic-conventions'; 28 | import { 29 | IncomingHttpHeaders, 30 | IncomingMessage, 31 | OutgoingHttpHeaders, 32 | RequestOptions, 33 | ServerResponse, 34 | } from 'http'; 35 | import { getRPCMetadata, RPCType } from '@opentelemetry/core'; 36 | import * as url from 'url'; 37 | import { AttributeNames } from './enums/AttributeNames.ts'; 38 | import { Err, IgnoreMatcher, ParsedRequestOptions } from './types.ts'; 39 | 40 | /** 41 | * Get an absolute url 42 | */ 43 | export const getAbsoluteUrl = ( 44 | requestUrl: ParsedRequestOptions | null, 45 | headers: IncomingHttpHeaders | OutgoingHttpHeaders, 46 | fallbackProtocol = 'http:' 47 | ): string => { 48 | const reqUrlObject = requestUrl || {}; 49 | const protocol = reqUrlObject.protocol || fallbackProtocol; 50 | const port = (reqUrlObject.port || '').toString(); 51 | const path = reqUrlObject.path || '/'; 52 | let host = 53 | reqUrlObject.host || reqUrlObject.hostname || headers.host || 'localhost'; 54 | 55 | // if there is no port in host and there is a port 56 | // it should be displayed if it's not 80 and 443 (default ports) 57 | if ( 58 | (host as string).indexOf(':') === -1 && 59 | port && 60 | port !== '80' && 61 | port !== '443' 62 | ) { 63 | host += `:${port}`; 64 | } 65 | 66 | return `${protocol}//${host}${path}`; 67 | }; 68 | 69 | /** 70 | * Parse status code from HTTP response. [More details](https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/data-http.md#status) 71 | */ 72 | export const parseResponseStatus = ( 73 | kind: SpanKind, 74 | statusCode?: number 75 | ): SpanStatusCode => { 76 | const upperBound = kind === SpanKind.CLIENT ? 400 : 500; 77 | // 1xx, 2xx, 3xx are OK on client and server 78 | // 4xx is OK on server 79 | if (statusCode && statusCode >= 100 && statusCode < upperBound) { 80 | return SpanStatusCode.UNSET; 81 | } 82 | 83 | // All other codes are error 84 | return SpanStatusCode.ERROR; 85 | }; 86 | 87 | /** 88 | * Check whether the given obj match pattern 89 | * @param constant e.g URL of request 90 | * @param pattern Match pattern 91 | */ 92 | export const satisfiesPattern = ( 93 | constant: string, 94 | pattern: IgnoreMatcher 95 | ): boolean => { 96 | if (typeof pattern === 'string') { 97 | return pattern === constant; 98 | } else if (pattern instanceof RegExp) { 99 | return pattern.test(constant); 100 | } else if (typeof pattern === 'function') { 101 | return pattern(constant); 102 | } else { 103 | throw new TypeError('Pattern is in unsupported datatype'); 104 | } 105 | }; 106 | 107 | /** 108 | * Check whether the given request is ignored by configuration 109 | * It will not re-throw exceptions from `list` provided by the client 110 | * @param constant e.g URL of request 111 | * @param [list] List of ignore patterns 112 | * @param [onException] callback for doing something when an exception has 113 | * occurred 114 | */ 115 | export const isIgnored = ( 116 | constant: string, 117 | list?: IgnoreMatcher[], 118 | onException?: (error: unknown) => void 119 | ): boolean => { 120 | if (!list) { 121 | // No ignored urls - trace everything 122 | return false; 123 | } 124 | // Try/catch outside the loop for failing fast 125 | try { 126 | for (const pattern of list) { 127 | if (satisfiesPattern(constant, pattern)) { 128 | return true; 129 | } 130 | } 131 | } catch (e) { 132 | if (onException) { 133 | onException(e); 134 | } 135 | } 136 | 137 | return false; 138 | }; 139 | 140 | /** 141 | * Sets the span with the error passed in params 142 | * @param {Span} span the span that need to be set 143 | * @param {Error} error error that will be set to span 144 | */ 145 | export const setSpanWithError = (span: Span, error: Err): void => { 146 | const message = error.message; 147 | 148 | span.setAttributes({ 149 | [AttributeNames.HTTP_ERROR_NAME]: error.name, 150 | [AttributeNames.HTTP_ERROR_MESSAGE]: message, 151 | }); 152 | 153 | span.setStatus({ code: SpanStatusCode.ERROR, message }); 154 | span.recordException(error); 155 | }; 156 | 157 | /** 158 | * Adds attributes for request content-length and content-encoding HTTP headers 159 | * @param { IncomingMessage } Request object whose headers will be analyzed 160 | * @param { SpanAttributes } SpanAttributes object to be modified 161 | */ 162 | export const setRequestContentLengthAttribute = ( 163 | request: IncomingMessage, 164 | attributes: SpanAttributes 165 | ): void => { 166 | const length = getContentLength(request.headers); 167 | if (length === null) return; 168 | 169 | if (isCompressed(request.headers)) { 170 | attributes[SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH] = length; 171 | } else { 172 | attributes[SemanticAttributes.HTTP_REQUEST_CONTENT_LENGTH_UNCOMPRESSED] = 173 | length; 174 | } 175 | }; 176 | 177 | /** 178 | * Adds attributes for response content-length and content-encoding HTTP headers 179 | * @param { IncomingMessage } Response object whose headers will be analyzed 180 | * @param { SpanAttributes } SpanAttributes object to be modified 181 | */ 182 | export const setResponseContentLengthAttribute = ( 183 | response: IncomingMessage, 184 | attributes: SpanAttributes 185 | ): void => { 186 | const length = getContentLength(response.headers); 187 | if (length === null) return; 188 | 189 | if (isCompressed(response.headers)) { 190 | attributes[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH] = length; 191 | } else { 192 | attributes[SemanticAttributes.HTTP_RESPONSE_CONTENT_LENGTH_UNCOMPRESSED] = 193 | length; 194 | } 195 | }; 196 | 197 | function getContentLength( 198 | headers: OutgoingHttpHeaders | IncomingHttpHeaders 199 | ): number | null { 200 | const contentLengthHeader = headers['content-length']; 201 | if (contentLengthHeader === undefined) return null; 202 | 203 | const contentLength = parseInt(contentLengthHeader as string, 10); 204 | if (isNaN(contentLength)) return null; 205 | 206 | return contentLength; 207 | } 208 | 209 | export const isCompressed = ( 210 | headers: OutgoingHttpHeaders | IncomingHttpHeaders 211 | ): boolean => { 212 | const encoding = headers['content-encoding']; 213 | 214 | return !!encoding && encoding !== 'identity'; 215 | }; 216 | 217 | /** 218 | * Makes sure options is an url object 219 | * return an object with default value and parsed options 220 | * @param options original options for the request 221 | * @param [extraOptions] additional options for the request 222 | */ 223 | export const getRequestInfo = ( 224 | options: url.URL | RequestOptions | string, 225 | extraOptions?: RequestOptions 226 | ): { 227 | origin: string; 228 | pathname: string; 229 | method: string; 230 | optionsParsed: RequestOptions; 231 | } => { 232 | let pathname = '/'; 233 | let origin = ''; 234 | let optionsParsed: RequestOptions; 235 | if (typeof options === 'string') { 236 | optionsParsed = url.parse(options); 237 | pathname = (optionsParsed as url.UrlWithStringQuery).pathname || '/'; 238 | origin = `${optionsParsed.protocol || 'http:'}//${optionsParsed.host}`; 239 | if (extraOptions !== undefined) { 240 | Object.assign(optionsParsed, extraOptions); 241 | } 242 | } else if (options instanceof url.URL) { 243 | optionsParsed = { 244 | protocol: options.protocol, 245 | hostname: 246 | typeof options.hostname === 'string' && options.hostname.startsWith('[') 247 | ? options.hostname.slice(1, -1) 248 | : options.hostname, 249 | path: `${options.pathname || ''}${options.search || ''}`, 250 | }; 251 | if (options.port !== '') { 252 | optionsParsed.port = Number(options.port); 253 | } 254 | if (options.username || options.password) { 255 | optionsParsed.auth = `${options.username}:${options.password}`; 256 | } 257 | pathname = options.pathname; 258 | origin = options.origin; 259 | if (extraOptions !== undefined) { 260 | Object.assign(optionsParsed, extraOptions); 261 | } 262 | } else { 263 | optionsParsed = Object.assign( 264 | { protocol: options.host ? 'http:' : undefined }, 265 | options 266 | ); 267 | pathname = (options as url.URL).pathname; 268 | if (!pathname && optionsParsed.path) { 269 | pathname = url.parse(optionsParsed.path).pathname || '/'; 270 | } 271 | const hostname = 272 | optionsParsed.host || 273 | (optionsParsed.port != null 274 | ? `${optionsParsed.hostname}${optionsParsed.port}` 275 | : optionsParsed.hostname); 276 | origin = `${optionsParsed.protocol || 'http:'}//${hostname}`; 277 | } 278 | 279 | // some packages return method in lowercase.. 280 | // ensure upperCase for consistency 281 | const method = optionsParsed.method 282 | ? optionsParsed.method.toUpperCase() 283 | : 'GET'; 284 | 285 | return { origin, pathname, method, optionsParsed }; 286 | }; 287 | 288 | /** 289 | * Makes sure options is of type string or object 290 | * @param options for the request 291 | */ 292 | export const isValidOptionsType = (options: unknown): boolean => { 293 | if (!options) { 294 | return false; 295 | } 296 | 297 | const type = typeof options; 298 | return type === 'string' || (type === 'object' && !Array.isArray(options)); 299 | }; 300 | 301 | export const extractHostnameAndPort = ( 302 | requestOptions: Pick< 303 | ParsedRequestOptions, 304 | 'hostname' | 'host' | 'port' | 'protocol' 305 | > 306 | ): { hostname: string; port: number | string } => { 307 | if (requestOptions.hostname && requestOptions.port) { 308 | return { hostname: requestOptions.hostname, port: requestOptions.port }; 309 | } 310 | const matches = requestOptions.host?.match(/^([^:/ ]+)(:\d{1,5})?/) || null; 311 | const hostname = 312 | requestOptions.hostname || (matches === null ? 'localhost' : matches[1]); 313 | let port = requestOptions.port; 314 | if (!port) { 315 | if (matches && matches[2]) { 316 | // remove the leading ":". The extracted port would be something like ":8080" 317 | port = matches[2].substring(1); 318 | } else { 319 | port = requestOptions.protocol === 'https:' ? '443' : '80'; 320 | } 321 | } 322 | return { hostname, port }; 323 | }; 324 | 325 | /** 326 | * Returns outgoing request attributes scoped to the options passed to the request 327 | * @param {ParsedRequestOptions} requestOptions the same options used to make the request 328 | * @param {{ component: string, hostname: string, hookAttributes?: SpanAttributes }} options used to pass data needed to create attributes 329 | */ 330 | export const getOutgoingRequestAttributes = ( 331 | requestOptions: ParsedRequestOptions, 332 | options: { 333 | component: string; 334 | hostname: string; 335 | port: string | number; 336 | hookAttributes?: SpanAttributes; 337 | } 338 | ): SpanAttributes => { 339 | const hostname = options.hostname; 340 | const port = options.port; 341 | const requestMethod = requestOptions.method; 342 | const method = requestMethod ? requestMethod.toUpperCase() : 'GET'; 343 | const headers = requestOptions.headers || {}; 344 | const userAgent = headers['user-agent']; 345 | const attributes: SpanAttributes = { 346 | [SemanticAttributes.HTTP_URL]: getAbsoluteUrl( 347 | requestOptions, 348 | headers, 349 | `${options.component}:` 350 | ), 351 | [SemanticAttributes.HTTP_METHOD]: method, 352 | [SemanticAttributes.HTTP_TARGET]: requestOptions.path || '/', 353 | [SemanticAttributes.NET_PEER_NAME]: hostname, 354 | [SemanticAttributes.HTTP_HOST]: 355 | requestOptions.headers?.host ?? `${hostname}:${port}`, 356 | }; 357 | 358 | if (userAgent !== undefined) { 359 | attributes[SemanticAttributes.HTTP_USER_AGENT] = userAgent; 360 | } 361 | return Object.assign(attributes, options.hookAttributes); 362 | }; 363 | 364 | /** 365 | * Returns outgoing request Metric attributes scoped to the request data 366 | * @param {SpanAttributes} spanAttributes the span attributes 367 | */ 368 | export const getOutgoingRequestMetricAttributes = ( 369 | spanAttributes: SpanAttributes 370 | ): MetricAttributes => { 371 | const metricAttributes: MetricAttributes = {}; 372 | metricAttributes[SemanticAttributes.HTTP_METHOD] = 373 | spanAttributes[SemanticAttributes.HTTP_METHOD]; 374 | metricAttributes[SemanticAttributes.NET_PEER_NAME] = 375 | spanAttributes[SemanticAttributes.NET_PEER_NAME]; 376 | //TODO: http.url attribute, it should susbtitute any parameters to avoid high cardinality. 377 | return metricAttributes; 378 | }; 379 | 380 | /** 381 | * Returns attributes related to the kind of HTTP protocol used 382 | * @param {string} [kind] Kind of HTTP protocol used: "1.0", "1.1", "2", "SPDY" or "QUIC". 383 | */ 384 | export const getAttributesFromHttpKind = (kind?: string): SpanAttributes => { 385 | const attributes: SpanAttributes = {}; 386 | if (kind) { 387 | attributes[SemanticAttributes.HTTP_FLAVOR] = kind; 388 | if (kind.toUpperCase() !== 'QUIC') { 389 | attributes[SemanticAttributes.NET_TRANSPORT] = NetTransportValues.IP_TCP; 390 | } else { 391 | attributes[SemanticAttributes.NET_TRANSPORT] = NetTransportValues.IP_UDP; 392 | } 393 | } 394 | return attributes; 395 | }; 396 | 397 | /** 398 | * Returns outgoing request attributes scoped to the response data 399 | * @param {IncomingMessage} response the response object 400 | * @param {{ hostname: string }} options used to pass data needed to create attributes 401 | */ 402 | export const getOutgoingRequestAttributesOnResponse = ( 403 | response: IncomingMessage 404 | ): SpanAttributes => { 405 | const { statusCode, statusMessage, httpVersion, socket } = response; 406 | const attributes: SpanAttributes = {}; 407 | if (socket) { 408 | const { remoteAddress, remotePort } = socket; 409 | attributes[SemanticAttributes.NET_PEER_IP] = remoteAddress; 410 | attributes[SemanticAttributes.NET_PEER_PORT] = remotePort; 411 | } 412 | setResponseContentLengthAttribute(response, attributes); 413 | 414 | if (statusCode) { 415 | attributes[SemanticAttributes.HTTP_STATUS_CODE] = statusCode; 416 | attributes[AttributeNames.HTTP_STATUS_TEXT] = ( 417 | statusMessage || '' 418 | ).toUpperCase(); 419 | } 420 | 421 | const httpKindAttributes = getAttributesFromHttpKind(httpVersion); 422 | return Object.assign(attributes, httpKindAttributes); 423 | }; 424 | 425 | /** 426 | * Returns outgoing request Metric attributes scoped to the response data 427 | * @param {SpanAttributes} spanAttributes the span attributes 428 | */ 429 | export const getOutgoingRequestMetricAttributesOnResponse = ( 430 | spanAttributes: SpanAttributes 431 | ): MetricAttributes => { 432 | const metricAttributes: MetricAttributes = {}; 433 | metricAttributes[SemanticAttributes.NET_PEER_PORT] = 434 | spanAttributes[SemanticAttributes.NET_PEER_PORT]; 435 | metricAttributes[SemanticAttributes.HTTP_STATUS_CODE] = 436 | spanAttributes[SemanticAttributes.HTTP_STATUS_CODE]; 437 | metricAttributes[SemanticAttributes.HTTP_FLAVOR] = 438 | spanAttributes[SemanticAttributes.HTTP_FLAVOR]; 439 | return metricAttributes; 440 | }; 441 | 442 | /** 443 | * Returns incoming request attributes scoped to the request data 444 | * @param {IncomingMessage} request the request object 445 | * @param {{ component: string, serverName?: string, hookAttributes?: SpanAttributes }} options used to pass data needed to create attributes 446 | */ 447 | export const getIncomingRequestAttributes = ( 448 | request: IncomingMessage, 449 | options: { 450 | component: string; 451 | serverName?: string; 452 | hookAttributes?: SpanAttributes; 453 | } 454 | ): SpanAttributes => { 455 | const headers = request.headers; 456 | const userAgent = headers['user-agent']; 457 | const ips = headers['x-forwarded-for']; 458 | const method = request.method || 'GET'; 459 | const httpVersion = request.httpVersion; 460 | const requestUrl = request.url ? url.parse(request.url) : null; 461 | const host = requestUrl?.host || headers.host; 462 | const hostname = 463 | requestUrl?.hostname || 464 | host?.replace(/^(.*)(:[0-9]{1,5})/, '$1') || 465 | 'localhost'; 466 | const serverName = options.serverName; 467 | const attributes: SpanAttributes = { 468 | [SemanticAttributes.HTTP_URL]: getAbsoluteUrl( 469 | requestUrl, 470 | headers, 471 | `${options.component}:` 472 | ), 473 | [SemanticAttributes.HTTP_HOST]: host, 474 | [SemanticAttributes.NET_HOST_NAME]: hostname, 475 | [SemanticAttributes.HTTP_METHOD]: method, 476 | [SemanticAttributes.HTTP_SCHEME]: options.component, 477 | }; 478 | 479 | if (typeof ips === 'string') { 480 | attributes[SemanticAttributes.HTTP_CLIENT_IP] = ips.split(',')[0]; 481 | } 482 | 483 | if (typeof serverName === 'string') { 484 | attributes[SemanticAttributes.HTTP_SERVER_NAME] = serverName; 485 | } 486 | 487 | if (requestUrl) { 488 | attributes[SemanticAttributes.HTTP_TARGET] = requestUrl.path || '/'; 489 | } 490 | 491 | if (userAgent !== undefined) { 492 | attributes[SemanticAttributes.HTTP_USER_AGENT] = userAgent; 493 | } 494 | setRequestContentLengthAttribute(request, attributes); 495 | 496 | const httpKindAttributes = getAttributesFromHttpKind(httpVersion); 497 | return Object.assign(attributes, httpKindAttributes, options.hookAttributes); 498 | }; 499 | 500 | /** 501 | * Returns incoming request Metric attributes scoped to the request data 502 | * @param {SpanAttributes} spanAttributes the span attributes 503 | * @param {{ component: string }} options used to pass data needed to create attributes 504 | */ 505 | export const getIncomingRequestMetricAttributes = ( 506 | spanAttributes: SpanAttributes 507 | ): MetricAttributes => { 508 | const metricAttributes: MetricAttributes = {}; 509 | metricAttributes[SemanticAttributes.HTTP_SCHEME] = 510 | spanAttributes[SemanticAttributes.HTTP_SCHEME]; 511 | metricAttributes[SemanticAttributes.HTTP_METHOD] = 512 | spanAttributes[SemanticAttributes.HTTP_METHOD]; 513 | metricAttributes[SemanticAttributes.NET_HOST_NAME] = 514 | spanAttributes[SemanticAttributes.NET_HOST_NAME]; 515 | metricAttributes[SemanticAttributes.HTTP_FLAVOR] = 516 | spanAttributes[SemanticAttributes.HTTP_FLAVOR]; 517 | //TODO: http.target attribute, it should susbtitute any parameters to avoid high cardinality. 518 | return metricAttributes; 519 | }; 520 | 521 | /** 522 | * Returns incoming request attributes scoped to the response data 523 | * @param {(ServerResponse & { socket: Socket; })} response the response object 524 | */ 525 | export const getIncomingRequestAttributesOnResponse = ( 526 | request: IncomingMessage, 527 | response: ServerResponse 528 | ): SpanAttributes => { 529 | // take socket from the request, 530 | // since it may be detached from the response object in keep-alive mode 531 | const { socket } = request; 532 | const { statusCode, statusMessage } = response; 533 | 534 | const rpcMetadata = getRPCMetadata(context.active()); 535 | const attributes: SpanAttributes = {}; 536 | if (socket) { 537 | const { localAddress, localPort, remoteAddress, remotePort } = socket; 538 | attributes[SemanticAttributes.NET_HOST_IP] = localAddress; 539 | attributes[SemanticAttributes.NET_HOST_PORT] = localPort; 540 | attributes[SemanticAttributes.NET_PEER_IP] = remoteAddress; 541 | attributes[SemanticAttributes.NET_PEER_PORT] = remotePort; 542 | } 543 | attributes[SemanticAttributes.HTTP_STATUS_CODE] = statusCode; 544 | attributes[AttributeNames.HTTP_STATUS_TEXT] = ( 545 | statusMessage || '' 546 | ).toUpperCase(); 547 | 548 | if (rpcMetadata?.type === RPCType.HTTP && rpcMetadata.route !== undefined) { 549 | attributes[SemanticAttributes.HTTP_ROUTE] = rpcMetadata.route; 550 | } 551 | return attributes; 552 | }; 553 | 554 | /** 555 | * Returns incoming request Metric attributes scoped to the request data 556 | * @param {SpanAttributes} spanAttributes the span attributes 557 | */ 558 | export const getIncomingRequestMetricAttributesOnResponse = ( 559 | spanAttributes: SpanAttributes 560 | ): MetricAttributes => { 561 | const metricAttributes: MetricAttributes = {}; 562 | metricAttributes[SemanticAttributes.HTTP_STATUS_CODE] = 563 | spanAttributes[SemanticAttributes.HTTP_STATUS_CODE]; 564 | metricAttributes[SemanticAttributes.NET_HOST_PORT] = 565 | spanAttributes[SemanticAttributes.NET_HOST_PORT]; 566 | if (spanAttributes[SemanticAttributes.HTTP_ROUTE] !== undefined) { 567 | metricAttributes[SemanticAttributes.HTTP_ROUTE] = 568 | spanAttributes[SemanticAttributes.HTTP_ROUTE]; 569 | } 570 | return metricAttributes; 571 | }; 572 | 573 | export function headerCapture(type: 'request' | 'response', headers: string[]) { 574 | const normalizedHeaders = new Map( 575 | headers.map(header => [ 576 | header.toLowerCase(), 577 | header.toLowerCase().replace(/-/g, '_'), 578 | ]) 579 | ); 580 | 581 | return ( 582 | span: Span, 583 | getHeader: (key: string) => undefined | string | string[] | number 584 | ) => { 585 | for (const [capturedHeader, normalizedHeader] of normalizedHeaders) { 586 | const value = getHeader(capturedHeader); 587 | 588 | if (value === undefined) { 589 | continue; 590 | } 591 | 592 | const key = `http.${type}.header.${normalizedHeader}`; 593 | 594 | if (typeof value === 'string') { 595 | span.setAttribute(key, [value]); 596 | } else if (Array.isArray(value)) { 597 | span.setAttribute(key, value); 598 | } else { 599 | span.setAttribute(key, [value]); 600 | } 601 | } 602 | }; 603 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export { BaselimeSDK } from './baselime.ts'; 2 | export { BetterHttpInstrumentation } from './http.ts'; 3 | export { StripePlugin } from './http-plugins/stripe.ts'; 4 | export { HttpPlugin } from './http-plugins/plugin.ts'; 5 | export { VercelPlugin } from './http-plugins/vercel.ts'; 6 | -------------------------------------------------------------------------------- /src/lambda.ts: -------------------------------------------------------------------------------- 1 | export { withOpenTelemetry } from './lambda/index.ts' -------------------------------------------------------------------------------- /src/lambda/index.ts: -------------------------------------------------------------------------------- 1 | import { trace, Context as OtelContext, Link} from "@opentelemetry/api"; 2 | import { Handler, Callback, Context } from "aws-lambda"; 3 | import { flatten } from "flat" 4 | import { flushTraces, captureError, setupTimeoutDetection, trackColdstart } from "./utils.ts"; 5 | import { parseInput } from "./parse-event.ts"; 6 | import { extractContext, injectContextToResponse } from "./propation.ts"; 7 | const tracer = trace.getTracer('@baselime/baselime-lambda-wrapper', '1'); 8 | 9 | type LambdaWrapperOptions = { 10 | proactiveInitializationThreshold?: number | undefined 11 | captureEvent?: boolean | undefined 12 | captureResponse?: boolean | undefined 13 | timeoutThreshold?: number | undefined 14 | extractContext?: (service: string, event: any) => { parent?: OtelContext, links?: Link[] } | void | undefined 15 | } 16 | 17 | const isColdstart = trackColdstart(); 18 | /** 19 | * Wrap a lambda handler with OpenTelemetry tracing 20 | * @param handler 21 | * @param opts 22 | * @returns 23 | */ 24 | export function withOpenTelemetry(handler: Handler, opts: LambdaWrapperOptions = {}) { 25 | return async function (event: any, lambda_context: Context, callback?: Callback) { 26 | 27 | const { coldstart, proactiveInitialization } = isColdstart(opts.proactiveInitializationThreshold); 28 | 29 | 30 | const { attributes, service } = parseInput(event, lambda_context, coldstart, proactiveInitialization, opts.captureEvent); 31 | 32 | const { links, parent } = extractContext(service, event, opts.extractContext); 33 | 34 | return tracer.startActiveSpan(lambda_context.functionName, { links, attributes }, parent, async (span) => { 35 | setupTimeoutDetection(span, lambda_context, opts.timeoutThreshold); 36 | try { 37 | let result = await handler(event, lambda_context, async (err, res) => { 38 | if (err) { captureError(span, err) } 39 | 40 | if (res) { 41 | if (opts.captureResponse) { 42 | span.setAttributes(flatten({ result: res })); 43 | } 44 | injectContextToResponse(service, res, span); 45 | } 46 | if (callback) { 47 | span.end(); 48 | await flushTraces(); 49 | return callback(err, res); 50 | } 51 | }); 52 | if (result) { 53 | if (opts.captureResponse) { 54 | span.setAttributes(flatten({ result })); 55 | } 56 | injectContextToResponse(service, result, span); 57 | } 58 | 59 | 60 | return result; 61 | } catch (e) { 62 | captureError(span, e); 63 | throw e; 64 | } finally { 65 | span.end(); 66 | await flushTraces(); 67 | } 68 | }); 69 | }; 70 | } 71 | -------------------------------------------------------------------------------- /src/lambda/parse-event.ts: -------------------------------------------------------------------------------- 1 | import { propagation, context, trace, Context, ROOT_CONTEXT, Attributes, Link, } from "@opentelemetry/api"; 2 | import { APIGatewayProxyEvent, APIGatewayProxyEventV2, DynamoDBStreamEvent, S3Event, Context as LambdaContext } from "aws-lambda"; 3 | import { flatten } from "flat"; 4 | 5 | function triggerToServiceType(service: string) { 6 | switch (service) { 7 | case "api": 8 | case "api-gateway": 9 | case "api-gateway-v2": 10 | case "function-url": 11 | return "http"; 12 | case "sns": 13 | case "sqs": 14 | case "kinesis": 15 | case "eventbridge": 16 | return "pubsub"; 17 | case "dynamodb": 18 | case "s3": 19 | return "datasource" 20 | default: 21 | return "other"; 22 | } 23 | } 24 | 25 | const DynamodbEventToDocumentOperations = { 26 | INSERT: 'insert', 27 | MODIFY: 'update', 28 | REMOVE: 'delete', 29 | default: '' 30 | }; 31 | 32 | function getDynamodbStreamDocumentAttributes(event: DynamoDBStreamEvent): FaasDocument { 33 | const unixTime = event?.Records[0]?.dynamodb?.ApproximateCreationDateTime || Date.now() / 1000; 34 | return { 35 | // TODO we could do better for collection (infer from single table design patterns?) 36 | collection: (event?.Records[0]?.eventSourceARN || '').split("/")[1], 37 | name: (event?.Records[0]?.eventSourceARN || '').split("/")[1], 38 | operation: DynamodbEventToDocumentOperations[event?.Records[0]?.eventName || "default"], 39 | time: new Date(unixTime).toUTCString(), 40 | } 41 | } 42 | 43 | function getS3DocumentAttributes(event: S3Event): FaasDocument { 44 | let operation = 'unkown'; 45 | 46 | if (event.Records[0].eventName.startsWith('ObjectCreated')) { 47 | operation = 'insert'; 48 | } 49 | 50 | if (event.Records[0].eventName.startsWith('ObjectRemoved')) { 51 | operation = 'delete'; 52 | } 53 | return { 54 | collection: event.Records[0].s3.bucket.name, 55 | name: event.Records[0].s3.object.key, 56 | operation, 57 | time: event.Records[0].eventTime, 58 | } 59 | } 60 | 61 | function parseJSON(str: string) { 62 | try { 63 | return JSON.parse(str); 64 | } catch (error) { 65 | return str; 66 | } 67 | } 68 | function parseHttpEvent(event: APIGatewayProxyEventV2 | APIGatewayProxyEvent): HttpEvent { 69 | try { 70 | if (event.headers['content-type']?.toLowerCase() === 'application/json') { 71 | return { 72 | body: parseJSON(event.body || '{}'), 73 | headers: event.headers 74 | }; 75 | } 76 | 77 | /** 78 | * TODO: add support for other content types 79 | */ 80 | 81 | return { 82 | body: event.body, 83 | headers: event.headers 84 | }; 85 | } catch (_) { 86 | return { 87 | body: event.body, 88 | headers: event.headers 89 | }; 90 | } 91 | 92 | } 93 | 94 | type FaasDocument = { 95 | collection: string 96 | operation: string 97 | time: string 98 | name: string 99 | } 100 | 101 | type HttpEvent = { body: unknown, headers: { [key: string]: string | undefined } }; 102 | 103 | 104 | 105 | function detectService(event: any) { 106 | if (event.requestContext?.apiId) { 107 | return "api-gateway"; 108 | } 109 | 110 | if (event.requestContext?.apiId && event.version === "2.0") { 111 | return "api-gateway-v2"; 112 | } 113 | 114 | if (event.Records && event.Records[0]?.EventSource === "aws:sns") { 115 | return "sns"; 116 | } 117 | 118 | if (event.Records && event.Records[0]?.eventSource === "aws:sqs") { 119 | return "sqs"; 120 | } 121 | 122 | if (event.Records && event.Records[0]?.eventSource === "aws:kinesis") { 123 | return "kinesis"; 124 | } 125 | 126 | if (event.Records && event.Records[0]?.eventSource === "aws:dynamodb") { 127 | return "dynamodb"; 128 | } 129 | 130 | if (event.Records && event.Records[0]?.eventSource === "aws:s3") { 131 | return "s3"; 132 | } 133 | 134 | if (event.Records && event.Records[0]?.eventSource === "aws:eventbridge") { 135 | return "eventbridge"; 136 | } 137 | 138 | if ( 139 | process.env.BASELIME_TRACE_STEP_FUNCTION === "true" || 140 | event.Payload?._baselime?.traceparent || event._baselime?.traceparent || 141 | (Array.isArray(event) && (event[0]?.Payload?.baselime?.traceparent || event[0]?._baselime?.traceparent)) 142 | ) { 143 | return "step-function"; 144 | } 145 | return 'unknown' 146 | } 147 | 148 | 149 | export function parseInput(event: any, lambda_context: LambdaContext, coldstart: boolean, proActiveInitialization: boolean, captureEvent: boolean = false) { 150 | 151 | const service = detectService(event); 152 | const trigger = triggerToServiceType(service); 153 | 154 | let document: FaasDocument | null = null; 155 | let httpEvent: HttpEvent | undefined = undefined; 156 | if (trigger === "http" && captureEvent) { 157 | httpEvent = parseHttpEvent(event); 158 | } 159 | if (trigger === 'datasource') { 160 | if (service === 'dynamodb') { 161 | document = getDynamodbStreamDocumentAttributes(event); 162 | } 163 | 164 | if (service === 's3') { 165 | document = getS3DocumentAttributes(event); 166 | } 167 | } 168 | 169 | const attributes = flatten({ 170 | event: captureEvent && (httpEvent || event), 171 | context: { 172 | functionName: lambda_context.functionName, 173 | functionVersion: lambda_context.functionVersion, 174 | invokedFunctionArn: lambda_context.invokedFunctionArn, 175 | memoryLimitInMB: lambda_context.memoryLimitInMB, 176 | awsRequestId: lambda_context.awsRequestId, 177 | logGroupName: lambda_context.logGroupName, 178 | logStreamName: lambda_context.logStreamName, 179 | identity: lambda_context.identity, 180 | clientContext: lambda_context.clientContext 181 | }, 182 | faas: { 183 | execution: lambda_context.awsRequestId, 184 | runtime: 'nodejs', 185 | trigger, 186 | document, 187 | invoked_by: service, 188 | id: lambda_context.invokedFunctionArn, 189 | coldstart, 190 | proActiveInitialization 191 | }, 192 | cloud: { 193 | resource_id: lambda_context.invokedFunctionArn, 194 | account_id: lambda_context.invokedFunctionArn.split(":")[4], 195 | } 196 | }) satisfies Attributes 197 | 198 | return { attributes, service } 199 | } -------------------------------------------------------------------------------- /src/lambda/propation.ts: -------------------------------------------------------------------------------- 1 | import { context, propagation, ROOT_CONTEXT, Link, Context, Span, trace } from "@opentelemetry/api"; 2 | 3 | const headerGetter = { 4 | keys(carrier: Object): string[] { 5 | return Object.keys(carrier); 6 | }, 7 | get(carrier: Record, key: string): string | undefined { 8 | return carrier[key]; 9 | }, 10 | }; 11 | 12 | const snsGetter = { 13 | keys(carrier: Object): string[] { 14 | return Object.keys(carrier); 15 | }, 16 | get(carrier: Record, key: string): string | undefined { 17 | return carrier[key]?.Value; 18 | }, 19 | }; 20 | 21 | 22 | export function extractContext(service: string, event: any, cb: (service: string, event: any) => { parent?: Context, links?: Link[]} | void | undefined): { parent?: Context, links?: Link[] } { 23 | if (cb) { 24 | const res = cb(service, event); 25 | if (res) { 26 | return res; 27 | } 28 | } 29 | switch (service) { 30 | case "api": 31 | case "api-gateway": 32 | case "api-gateway-v2": 33 | case "function-url": 34 | const httpHeaders = event.headers || {}; 35 | return { 36 | parent: propagation.extract(context.active(), httpHeaders, headerGetter), 37 | }; 38 | case "sns": 39 | return { 40 | parent: propagation.extract(context.active(), event.Records[0].Sns.MessageAttributes, snsGetter) 41 | } 42 | case 'step-function': 43 | if (Array.isArray(event)) { 44 | return { 45 | links: event.map((parent) => { 46 | const traceparent = parent._baselime?.traceparent || parent.Payload?._baselime?.traceparent || parent._baselime?._baselime?.traceparent; 47 | if (!traceparent) { 48 | return 49 | } 50 | return { 51 | context: { 52 | traceId: traceparent.split('-')[1], 53 | spanId: traceparent.split('-')[2], 54 | traceFlags: Number(traceparent.split('-')[3]), 55 | } 56 | } 57 | }).filter(el => el) 58 | } 59 | } 60 | const traceparent = event._baselime?.traceparent || event.Payload?._baselime?.traceparent || event._baselime?._baselime?.traceparent; 61 | 62 | return { 63 | parent: propagation.extract(context.active(), { traceparent }, headerGetter) 64 | } 65 | default: 66 | return { 67 | parent: ROOT_CONTEXT, 68 | }; 69 | } 70 | } 71 | 72 | export function injectContextToResponse(service: string, result: any, span: Span) { 73 | const ctx = trace.setSpan(context.active(), span); 74 | switch (service) { 75 | case 'step-function': 76 | propagation.inject(ctx, result, { 77 | set(carrier, key, value) { 78 | carrier['_baselime'] = { 79 | [key]: value 80 | } 81 | } 82 | }); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/lambda/utils.ts: -------------------------------------------------------------------------------- 1 | import { Span, trace } from "@opentelemetry/api"; 2 | import { Context } from "aws-lambda"; 3 | import { flatten } from "flat"; 4 | 5 | const timeoutErrorMessage = `The Baselime OpenTelemetry SDK has detected that this lambda is very close to timing out.` 6 | 7 | export function setupTimeoutDetection(span: Span, lambda_context: Context, timeoutThreshold: number = 500) { 8 | const timeRemaining = lambda_context.getRemainingTimeInMillis(); 9 | setTimeout(async () => { 10 | const error = new Error(timeoutErrorMessage); 11 | error.name = "Possible Lambda Timeout"; 12 | span.setAttributes(flatten({ error: { name: error.name, message: error.message } })); 13 | span.recordException(error); 14 | span.end(); 15 | await flushTraces(); 16 | }, timeRemaining - (timeoutThreshold)); 17 | } 18 | 19 | export function trackColdstart() { 20 | let coldstart = true; 21 | const startTime = Date.now(); 22 | return (proactiveInitializationThreshold: number = 1000) => { 23 | if (coldstart) { 24 | coldstart = false; 25 | const coldstartDuration = Date.now() - startTime; 26 | return { 27 | coldstart, 28 | coldstartDuration, 29 | proactiveInitialization: coldstartDuration > proactiveInitializationThreshold 30 | }; 31 | } 32 | return { coldstart }; 33 | } 34 | } 35 | 36 | export function captureError(span: Span, err: unknown) { 37 | let error = typeof err === 'string' ? new Error(err) : err as Error; 38 | span.recordException(error); 39 | span.setAttributes(flatten({ error: { name: error.name, message: error.message, stack: error.stack } })); 40 | } 41 | 42 | export async function flushTraces() { 43 | try { 44 | // @ts-expect-error 45 | await trace.getTracerProvider().getDelegate().forceFlush(); 46 | } catch (_) { 47 | } 48 | } -------------------------------------------------------------------------------- /src/resources/koyeb.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DetectorSync, 3 | Resource, 4 | ResourceDetectionConfig, 5 | } from '@opentelemetry/resources'; 6 | import { 7 | SemanticResourceAttributes, 8 | } from '@opentelemetry/semantic-conventions'; 9 | 10 | export class KoyebDetector implements DetectorSync { 11 | detect(_config?: ResourceDetectionConfig): Resource { 12 | if (!process.env.KOYEB_APP_NAME) { 13 | return Resource.empty(); 14 | 15 | } 16 | const portProtocols = Object.keys(process.env).filter((key) => key.startsWith('KOYEB_PORT_')); 17 | 18 | const protocols = portProtocols.reduce((sum, el) => ({ 19 | ...sum, 20 | [el.replace('_', '.').toLowerCase()]: process.env[el] 21 | }), {} as Record); 22 | 23 | const attributes = { 24 | [SemanticResourceAttributes.CLOUD_PROVIDER]: String( 25 | 'Koyeb' 26 | ), 27 | [SemanticResourceAttributes.CLOUD_PLATFORM]: String( 28 | 'Koyeb MicroVM' 29 | ), 30 | [SemanticResourceAttributes.CLOUD_REGION]: String(process.env.KOYEB_DC), 31 | 'koyeb.app.name': String(process.env.KOYEB_APP_NAME), 32 | 'koyeb.app.id': String(process.env.KOYEB_APP_ID), 33 | 'koyeb.organization.name': String(process.env.KOYEB_ORGANIZATION_NAME), 34 | 'koyeb.organization.id': String(process.env.KOYEB_ORGANIZATION_ID), 35 | 'koyeb.service.name': String(process.env.KOYEB_SERVICE_NAME), 36 | 'koyeb.service.id': String(process.env.KOYEB_SERVICE_ID), 37 | 'koyeb.service.privateDomain': String(process.env.KOYEB_SERVICE_PRIVATE_DOMAIN), 38 | 'koyeb.publicDomain': String(process.env.KOYEB_PUBLIC_DOMAIN), 39 | 'koyeb.region': String(process.env.KOYEB_REGION), 40 | 'koyeb.regionalDeploymentId': String(process.env.KOYEB_REGIONAL_DEPLOYMENT_ID), 41 | 'koyeb.instance.id': String(process.env.KOYEB_INSTANCE_ID), 42 | 'koyeb.instance.type': String(process.env.KOYEB_INSTANCE_TYPE), 43 | 'koyeb.instance.memory': String(process.env.KOYEB_INSTANCE_MEMORY_MB), 44 | 'koyeb.privileged': process.env.KOYEB_PRIVILEGED === 'true', 45 | 'koyeb.hypervisor.id': String(process.env.KOYEB_HYPERVISOR_ID), 46 | 'koyeb.dc': String(process.env.KOYEB_DC), 47 | 'koyeb.docker.ref': String(process.env.KOYEB_DOCKER_REF), 48 | 'koyeb.git.sha': String(process.env.KOYEB_GIT_SHA), 49 | 'koyeb.git.branch': String(process.env.KOYEB_GIT_BRANCH), 50 | 'koyeb.git.commit.author': String(process.env.KOYEB_GIT_COMMIT_AUTHOR), 51 | 'koyeb.git.commit.message': String(process.env.KOYEB_GIT_COMMIT_MESSAGE), 52 | 'koyeb.git.repository': String(process.env.KOYEB_GIT_REPOSITORY), 53 | ...protocols, 54 | 55 | // SET OTEL SPECIAL ATTRIBUTES 56 | 'service.name': String(process.env.KOYEB_APP_NAME), 57 | 'service.namespace': String(process.env.KOYEB_SERVICE_NAME), 58 | } 59 | 60 | 61 | 62 | return new Resource(attributes); 63 | } 64 | } -------------------------------------------------------------------------------- /src/resources/service.ts: -------------------------------------------------------------------------------- 1 | import { Attributes } from '@opentelemetry/api'; 2 | import { 3 | DetectorSync, 4 | Resource, 5 | ResourceDetectionConfig, 6 | } from '@opentelemetry/resources'; 7 | type ServiceDetectorConfig = { 8 | serviceName?: string, 9 | attributes?: Attributes | Resource, 10 | } 11 | 12 | export class ServiceDetector implements DetectorSync { 13 | serviceName?: string; 14 | attributes?: Attributes 15 | constructor(config?: ServiceDetectorConfig) { 16 | this.serviceName = config?.serviceName || process.env.OTEL_SERVICE_NAME; 17 | this.attributes = config?.attributes instanceof Resource ? config.attributes.attributes : config?.attributes || {}; 18 | } 19 | detect(_config?: ResourceDetectionConfig): Resource { 20 | if (!this.serviceName || !this.attributes) { 21 | return Resource.empty(); 22 | } 23 | 24 | const attributes = { 25 | 'service.name': this.serviceName, 26 | 'service.namespace': this.serviceName, 27 | ...this.attributes, 28 | } 29 | return new Resource(attributes); 30 | } 31 | } -------------------------------------------------------------------------------- /src/resources/vercel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DetectorSync, 3 | Resource, 4 | ResourceDetectionConfig, 5 | } from '@opentelemetry/resources'; 6 | import { 7 | SemanticResourceAttributes, 8 | SEMRESATTRS_CLOUD_PROVIDER, 9 | SEMRESATTRS_CLOUD_REGION, 10 | SEMRESATTRS_CLOUD_PLATFORM, 11 | } from '@opentelemetry/semantic-conventions'; 12 | 13 | export class VercelDetector implements DetectorSync { 14 | detect(_config?: ResourceDetectionConfig): Resource { 15 | if (!process.env.VERCEL) { 16 | return Resource.empty(); 17 | } 18 | 19 | const deploymentUrl = String(process.env.VERCEL_URL); 20 | 21 | if (!deploymentUrl) { 22 | return Resource.empty(); 23 | } 24 | 25 | 26 | const gitBranchUrl = String(process.env.VERCEL_BRANCH_URL); 27 | let serviceName: string; 28 | let serviceNamespace: string; 29 | 30 | if(gitBranchUrl) { 31 | try { 32 | serviceName = gitBranchUrl.split('-git-')[0] 33 | serviceNamespace = serviceName; 34 | } catch(e) { 35 | } 36 | } 37 | 38 | const attributes = { 39 | [SEMRESATTRS_CLOUD_PROVIDER]: String( 40 | 'Vercel' 41 | ), 42 | [SEMRESATTRS_CLOUD_PLATFORM]: String( 43 | 'Vercel Functions' 44 | ), 45 | [SEMRESATTRS_CLOUD_REGION]: String(process.env.VERCEL_REGION), 46 | 'vercel.environment': String(process.env.VERCEL_ENV), 47 | 'vercel.url': String(process.env.VERCEL_URL), 48 | 'vercel.url.branch': String(process.env.VERCEL_BRANCH_URL), 49 | 'vercel.git.provider': String(process.env.VERCEL_GIT_PROVIDER), 50 | 'vercel.git.repo': String(process.env.VERCEL_GIT_REPO_SLUG), 51 | 'vercel.git.commit': String(process.env.VERCEL_GIT_COMMIT_SHA), 52 | 'vercel.git.message': String(process.env.VERCEL_GIT_COMMIT_MESSAGE), 53 | 'vercel.git.author': String(process.env.VERCEL_GIT_COMMIT_AUTHOR_NAME), 54 | 'service.name': serviceName, 55 | 'service.namespace': serviceNamespace, 56 | } 57 | 58 | return new Resource(attributes); 59 | } 60 | } -------------------------------------------------------------------------------- /src/trpc.ts: -------------------------------------------------------------------------------- 1 | import { trace, Span } from '@opentelemetry/api'; 2 | import { experimental_standaloneMiddleware } from '@trpc/server'; 3 | import { flatten } from 'flat'; 4 | 5 | type TracingOptions = { 6 | collectInput?: boolean, 7 | collectResult?: boolean, 8 | instrumentedContextFields?: string[], 9 | headers?: string[] 10 | } 11 | 12 | /** 13 | * @param options 14 | * @param options.collectInput - Whether or not to collect the input of the request. Defaults to false. 15 | * 16 | * @returns 17 | */ 18 | export function tracing(options?: TracingOptions) { 19 | const tracer = trace.getTracer('@baselime/trpc'); 20 | options = options || {}; 21 | return experimental_standaloneMiddleware().create(async (opts) => { 22 | return tracer.startActiveSpan(`TRPC ${opts.type}`, async (span: Span) => { 23 | const result = await opts.next(); 24 | 25 | // opts.rawInput is for v10, `opts.getRawInput` is for v11 26 | // @ts-expect-error 27 | const rawInput = "rawInput" in opts ? opts.rawInput : await opts.getRawInput(); 28 | if (options.collectInput && typeof rawInput === "object") { 29 | span.setAttributes(flatten({ input: rawInput })) 30 | } 31 | const meta = { path: opts.path, type: opts.type, ok: result.ok }; 32 | span.setAttributes(meta) 33 | span.end(); 34 | return result; 35 | }); 36 | }); 37 | } -------------------------------------------------------------------------------- /src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export function safely(cb: () => any) { 4 | try { 5 | return cb(); 6 | } catch (e) { 7 | return undefined; 8 | } 9 | } -------------------------------------------------------------------------------- /tests/http.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, beforeEach, afterEach } from 'vitest'; 2 | import { getLocal, completionCheckers, MockedEndpoint, CompletedRequest } from 'mockttp'; 3 | import { BaselimeSDK, betterHttpInstrumentation } from "../src/index"; 4 | import { trace } from "@opentelemetry/api" 5 | import { getSpans, waitForCollector } from './utils/otel'; 6 | import { VercelPlugin } from '../src/http-plugins/vercel'; 7 | import { request } from 'undici'; 8 | 9 | describe("Test BaselimeSDK for opentelemetry", async () => { 10 | 11 | const mockServer = getLocal(); 12 | beforeEach(() => mockServer.start()) 13 | afterEach(() => mockServer.stop()) 14 | 15 | 16 | it.skip("HTTP Vercel HEADERS!", async () => { 17 | const collector = await mockServer.forAnyRequest().once().thenReply(200, "Ok"); 18 | 19 | const vercelServer = getLocal(); 20 | await vercelServer.start(); 21 | const fakeVercel = await vercelServer.forAnyRequest().once().thenReply(200, "Ok"); 22 | const baselimeKey = "love is magic" 23 | const sdk = new BaselimeSDK({ 24 | collectorUrl: mockServer.url, 25 | serverless: true, 26 | baselimeKey: baselimeKey, 27 | instrumentations: [ 28 | 29 | ] 30 | }) 31 | 32 | sdk.start(); 33 | 34 | // Create a span 35 | await request(vercelServer.url, { 36 | headers: { 37 | 'x-vercel-id': 'test::test::test', 38 | } 39 | }) 40 | 41 | 42 | const [traceRequest] = await waitForCollector(collector) 43 | 44 | expect(traceRequest.headers["x-api-key"]).toBe(baselimeKey); 45 | 46 | 47 | const { resourceSpans: [serialisedSpan]} = getSpans(traceRequest) 48 | 49 | console.log(JSON.stringify(serialisedSpan, null, 2)) 50 | expect(serialisedSpan.resource.attributes.find((attr) => attr.key === "$baselime.service")?.value.stringValue).toBe("my_service"); 51 | expect(serialisedSpan.resource.attributes.find((attr) => attr.key === "$baselime.namespace")?.value.stringValue).toBe("my_namespace") 52 | }) 53 | }); -------------------------------------------------------------------------------- /tests/index.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, beforeEach, afterEach } from 'vitest'; 2 | import { getLocal, completionCheckers, MockedEndpoint, CompletedRequest } from 'mockttp'; 3 | import { BaselimeSDK } from "../src/index"; 4 | import { trace } from "@opentelemetry/api" 5 | import { getSpans, waitForCollector } from './utils/otel'; 6 | 7 | describe("Test BaselimeSDK for opentelemetry", async () => { 8 | 9 | const mockServer = getLocal(); 10 | beforeEach(() => mockServer.start()) 11 | afterEach(() => mockServer.stop()) 12 | 13 | 14 | it("Traces are recieved", async () => { 15 | const collector = await mockServer.forAnyRequest().once().thenReply(200, "Ok"); 16 | const baselimeKey = "love is magic" 17 | const sdk = new BaselimeSDK({ 18 | collectorUrl: mockServer.url, 19 | serverless: true, 20 | baselimeKey: baselimeKey, 21 | }) 22 | 23 | sdk.start(); 24 | 25 | // Create a span 26 | 27 | const span = trace.getTracer("test").startSpan("test"); 28 | 29 | 30 | span.end(); 31 | 32 | 33 | const [request] = await waitForCollector(collector) 34 | 35 | expect(request.headers["x-api-key"]).toBe(baselimeKey); 36 | }) 37 | }); -------------------------------------------------------------------------------- /tests/utils/otel.ts: -------------------------------------------------------------------------------- 1 | import { CompletedRequest, MockedEndpoint } from "mockttp"; 2 | 3 | 4 | export function waitForCollector(collector: MockedEndpoint): Promise { 5 | return new Promise(async (resolve) => { 6 | let pending = true; 7 | while (pending) { 8 | await new Promise((resolve) => setTimeout(resolve, 10)) 9 | pending = await collector.isPending(); 10 | } 11 | const requests = await collector.getSeenRequests(); 12 | resolve(requests); 13 | }); 14 | } 15 | 16 | export type SerialisedSpan = { 17 | resourceSpans: Array<{ 18 | resource: { 19 | attributes: Array<{ 20 | key: string 21 | value: { 22 | stringValue?: string 23 | intValue?: number 24 | arrayValue?: { 25 | values: Array<{ 26 | stringValue: string 27 | }> 28 | } 29 | } 30 | }> 31 | droppedAttributesCount: number 32 | } 33 | scopeSpans: Array<{ 34 | scope: { 35 | name: string 36 | } 37 | spans: Array<{ 38 | traceId: string 39 | spanId: string 40 | name: string 41 | kind: number 42 | startTimeUnixNano: { 43 | low: number 44 | high: number 45 | } 46 | endTimeUnixNano: { 47 | low: number 48 | high: number 49 | } 50 | attributes: Array 51 | droppedAttributesCount: number 52 | events: Array 53 | droppedEventsCount: number 54 | status: { 55 | code: number 56 | } 57 | links: Array 58 | droppedLinksCount: number 59 | }> 60 | }> 61 | }> 62 | } 63 | 64 | 65 | export function getSpans(request: CompletedRequest): SerialisedSpan { 66 | return JSON.parse(request.body.buffer.toString()); 67 | } -------------------------------------------------------------------------------- /trace-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baselime/node-opentelemetry/1a4b1722e0cae69c23618350b35ecd7a5e421331/trace-2.png -------------------------------------------------------------------------------- /traces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baselime/node-opentelemetry/1a4b1722e0cae69c23618350b35ecd7a5e421331/traces.png -------------------------------------------------------------------------------- /trpc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baselime/node-opentelemetry/1a4b1722e0cae69c23618350b35ecd7a5e421331/trpc.png -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "nodenext", 4 | "allowImportingTsExtensions": true, 5 | "noEmit": true, 6 | "declaration": true, 7 | "moduleResolution": "nodenext", 8 | "noImplicitAny": true, 9 | "removeComments": true, 10 | "preserveConstEnums": true, 11 | "sourceMap": true, 12 | 13 | }, 14 | "include": [ 15 | "src/**/*" 16 | ], 17 | "exclude": [ 18 | "node_modules", 19 | "**/*.spec.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tslint:latest" 3 | } 4 | -------------------------------------------------------------------------------- /tsup.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup' 2 | 3 | export default defineConfig({ 4 | entry: ['src/index.ts', 'src/lambda.ts', 'src/trpc.ts'], 5 | splitting: false, 6 | sourcemap: false, 7 | dts: true, 8 | clean: true, 9 | format: ['cjs'], 10 | target: 'node18', 11 | minify: false, 12 | metafile: true, 13 | // for now we include flat in the bundle because it is not exported correctly for both esm and cjs 14 | noExternal: [/flat/, /opentelemetry/], 15 | }) 16 | --------------------------------------------------------------------------------