├── .eslintignore ├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml ├── dependabot.yml └── workflows │ ├── README │ └── ci.yml ├── .gitignore ├── .husky └── commit-msg ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── SECURITY.md ├── commitlint.config.js ├── docs ├── README.md ├── favicon.png ├── introduction.mdx ├── jest-otel │ ├── architecture.mdx │ ├── architecture.png │ ├── contributing │ │ ├── local-env.mdx │ │ ├── overview.mdx │ │ └── test-servers.mdx │ ├── getting-started.mdx │ ├── introduction.mdx │ ├── no-otel.mdx │ └── syntax │ │ ├── db-pg.mdx │ │ ├── db-redis.mdx │ │ ├── overview.mdx │ │ ├── services-grpc.mdx │ │ └── services-rest.mdx ├── logo │ ├── dark.svg │ └── light.svg └── mint.json ├── img └── logo.png ├── jest-opentelemetry.config.js ├── jest.config.js ├── lerna.json ├── package-lock.json ├── package.json ├── packages ├── expect-opentelemetry │ ├── .npmignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.mjs │ └── src │ │ ├── index.ts │ │ ├── matchers │ │ ├── index.ts │ │ ├── service │ │ │ ├── grpc-request.test.ts │ │ │ ├── http-request.test.ts │ │ │ ├── index.ts │ │ │ ├── to-query-postgresql.test.ts │ │ │ ├── to-query-postgresql.ts │ │ │ ├── to-receive-grpc-request.ts │ │ │ ├── to-receive-http-request.ts │ │ │ ├── to-send-grpc-request.ts │ │ │ ├── to-send-http-request.ts │ │ │ └── to-send-redis-command.ts │ │ └── utils │ │ │ ├── comparators.ts │ │ │ ├── compare-types.ts │ │ │ ├── filters.ts │ │ │ └── index.ts │ │ ├── options.ts │ │ ├── resources │ │ ├── grpc-request.test.ts │ │ ├── grpc-request.ts │ │ ├── http-request.test.ts │ │ ├── http-request.ts │ │ ├── index.ts │ │ ├── postgresql-query.ts │ │ ├── redis-command.ts │ │ └── service.ts │ │ ├── trace-loop │ │ ├── fetch-traces.ts │ │ ├── filter-service-spans.ts │ │ └── index.ts │ │ └── utils.ts ├── instrument-opentelemetry │ ├── .dockerignore │ ├── CHANGELOG.md │ ├── Dockerfile │ ├── package.json │ ├── rollup.config.mjs │ └── src │ │ ├── otel-custom │ │ ├── constants.js │ │ ├── express.js │ │ ├── http.js │ │ ├── mime-type.js │ │ └── stream-chunks.js │ │ └── tracing.ts ├── jest-environment-otel │ ├── .npmignore │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── index.js │ ├── jest.config.js │ ├── package.json │ ├── rollup.config.mjs │ ├── setup.js │ ├── src │ │ ├── env.js │ │ ├── global.ts │ │ └── readConfig.ts │ └── teardown.js ├── jest-opentelemetry │ ├── CHANGELOG.md │ ├── LICENSE │ ├── README.md │ ├── jest-preset.js │ └── package.json ├── otel-proto │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── rollup.config.mjs │ └── src │ │ ├── index.d.ts │ │ ├── index.js │ │ ├── opentelemetry │ │ └── proto │ │ │ ├── common │ │ │ └── v1 │ │ │ │ └── common.proto │ │ │ ├── resource │ │ │ └── v1 │ │ │ │ └── resource.proto │ │ │ └── trace │ │ │ └── v1 │ │ │ └── trace.proto │ │ └── traceloop │ │ └── proto │ │ └── v1 │ │ └── traceloop.proto ├── otel-receiver │ ├── CHANGELOG.md │ ├── LICENSE │ ├── package.json │ ├── rollup.config.mjs │ └── src │ │ ├── index.ts │ │ └── store.ts └── test-servers │ ├── .dockerignore │ ├── CHANGELOG.md │ ├── Dockerfile │ ├── deployment.yaml │ ├── package.json │ ├── rollup.config.mjs │ └── src │ ├── helloworld.proto │ └── index.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | jest.config.js 2 | tsconfig.json 3 | dist/ 4 | packages/otel-proto 5 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es2021": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "prettier" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "parserOptions": { 14 | "ecmaVersion": "latest", 15 | "sourceType": "module" 16 | }, 17 | "plugins": ["@typescript-eslint"] 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: '🐛 Bug Report' 2 | description: 'Submit a bug report to help us improve' 3 | title: '🐛 Bug Report: ' 4 | labels: ['type: bug'] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: We value your time and effort to submit this bug report. 🙏 9 | - type: textarea 10 | id: description 11 | validations: 12 | required: true 13 | attributes: 14 | label: '📜 Description' 15 | description: 'A clear and concise description of what the bug is.' 16 | placeholder: 'It bugs out when ...' 17 | - type: textarea 18 | id: steps-to-reproduce 19 | validations: 20 | required: true 21 | attributes: 22 | label: '👟 Reproduction steps' 23 | description: 'How do you trigger this bug? Please walk us through it step by step.' 24 | placeholder: "1. Go to '...' 25 | 2. Click on '....' 26 | 3. Scroll down to '....' 27 | 4. See error" 28 | - type: textarea 29 | id: expected-behavior 30 | validations: 31 | required: true 32 | attributes: 33 | label: '👍 Expected behavior' 34 | description: 'What did you think should happen?' 35 | placeholder: 'It should ...' 36 | - type: textarea 37 | id: actual-behavior 38 | validations: 39 | required: true 40 | attributes: 41 | label: '👎 Actual Behavior with Screenshots' 42 | description: 'What did actually happen? Add screenshots, if applicable.' 43 | placeholder: 'It actually ...' 44 | - type: input 45 | id: node-version 46 | validations: 47 | required: false 48 | attributes: 49 | label: '🤖 Node Version' 50 | description: > 51 | What Node version are you using? 52 | - type: textarea 53 | id: additional-context 54 | validations: 55 | required: false 56 | attributes: 57 | label: '📃 Provide any additional context for the Bug.' 58 | description: 'Add any other context about the problem here.' 59 | placeholder: 'It actually ...' 60 | - type: checkboxes 61 | id: no-duplicate-issues 62 | attributes: 63 | label: '👀 Have you spent some time to check if this bug has been raised before?' 64 | options: 65 | - label: "I checked and didn't find similar issue" 66 | required: true 67 | - type: dropdown 68 | attributes: 69 | label: Are you willing to submit PR? 70 | description: This is absolutely not required, but we are happy to guide you in the contribution process. 71 | options: 72 | - 'Yes I am willing to submit a PR!' 73 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature 2 | description: 'Submit a proposal for a new feature' 3 | title: '🚀 Feature: ' 4 | labels: [feature] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | We value your time and efforts to submit this Feature request form. 🙏 10 | - type: textarea 11 | id: feature-description 12 | validations: 13 | required: true 14 | attributes: 15 | label: '🔖 Feature description' 16 | description: 'A clear and concise description of what the feature is.' 17 | placeholder: 'You should add ...' 18 | - type: textarea 19 | id: pitch 20 | validations: 21 | required: true 22 | attributes: 23 | label: '🎤 Why is this feature needed ?' 24 | description: 'Please explain why this feature should be implemented and how it would be used. Add examples, if applicable.' 25 | placeholder: 'In my use-case, ...' 26 | - type: textarea 27 | id: solution 28 | validations: 29 | required: true 30 | attributes: 31 | label: '✌️ How do you aim to achieve this?' 32 | description: 'A clear and concise description of what you want to happen.' 33 | placeholder: 'I want this feature to, ...' 34 | - type: textarea 35 | id: alternative 36 | validations: 37 | required: false 38 | attributes: 39 | label: '🔄️ Additional Information' 40 | description: "A clear and concise description of any alternative solutions or additional solutions you've considered." 41 | placeholder: 'I tried, ...' 42 | - type: checkboxes 43 | id: no-duplicate-issues 44 | attributes: 45 | label: '👀 Have you spent some time to check if this feature request has been raised before?' 46 | options: 47 | - label: "I checked and didn't find similar issue" 48 | required: true 49 | - type: dropdown 50 | id: willing-to-submit-pr 51 | attributes: 52 | label: Are you willing to submit PR? 53 | description: This is absolutely not required, but we are happy to guide you in the contribution process. 54 | options: 55 | - 'Yes I am willing to submit a PR!' 56 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | versioning-strategy: increase 8 | -------------------------------------------------------------------------------- /.github/workflows/README: -------------------------------------------------------------------------------- 1 | # Debugging GitHub workflows locally 2 | 3 | 1. Install [act](https://github.com/nektos/act) by running `brew install act` 4 | 2. Install [Docker Desktop](https://www.docker.com/products/docker-desktop/) (if not installed) 5 | 3. Create a [Github token](https://github.com/settings/tokens) 6 | 4. Run `act -l` to list all available events and actions 7 | 5. Run `act -s GITHUB_TOKEN= -j` on the root directory of the repo (see [here](https://github.com/nektos/act#example-commands) for more options) to run the job you want to test. If you need to supply other secrets you can add them with `-s SECRET=`. 8 | 9 | ## Tip 10 | 11 | If you get an error like: 12 | 13 | ``` 14 | Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running? 15 | ``` 16 | 17 | Run `export DOCKER_HOST=$(docker context inspect --format '{{.Endpoints.docker.Host}}')` and then re-run `act` as written above. 18 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: 'CI' 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | Test: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | node-version: [16, 18] 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | - uses: actions/setup-node@v3 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - uses: harmon758/postgresql-action@v1 21 | with: 22 | postgresql version: '15' 23 | postgresql db: 'postgres' 24 | postgresql user: 'postgres' 25 | postgresql password: 'postgres' 26 | - run: npm ci 27 | - run: npm run build 28 | - run: npm run lint 29 | - run: npm run test-ci 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | lib/ 107 | 108 | .DS_Store -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit 5 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16.15 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | *.proto 2 | dist/ 3 | packages/otel-proto 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "trailingComma": "all", 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | support@traceloop.dev. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to jest-opentelemetry 2 | 3 | Thanks for taking the time to contribute! 😃 🚀 4 | 5 | Please refer to our [Contributing Guide](https://traceloop.com/docs/jest-otel/contributing/overview) for instructions on how to contribute. 6 | -------------------------------------------------------------------------------- /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 [yyyy] [name of copyright owner] 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 |

2 | 3 | 4 | 5 |

6 |

Jest OpenTelemetry

7 |

8 |

Integeration tests with 10 lines of code

9 |

10 |

11 | Get started » 12 |
13 |
14 | Slack | 15 | Docs | 16 | Examples | 17 | Website 18 |

19 | 20 |

21 | 22 | Jest OpenTelemetry is released under the Apache-2.0 License 23 | 24 | 25 | 26 | PRs welcome! 27 | 28 | 29 | git commit activity 30 | 31 | 32 | Slack community channel 33 | 34 | 35 | Traceloop Twitter 36 | 37 |

38 | 39 | Jest OpenTelemetry allows you to write, build and run integration tests based on OpenTelemetry traces with [Jest-like](https://jestjs.io/) syntax. You can: 40 | 41 | - 🕵️ Test any side-effect in your system: an email was sent, a database was updated, a BI event was reported, etc. 42 | - 👩‍💻 Run your tests and connect to a local or remote test environment. 43 | 44 | All with just a few lines of code. 45 | 46 | It's built and maintained by Traceloop under the Apache 2.0 license. 47 | 48 | ## 🚀 Getting Started 49 | 50 | ```js 51 | npm i --save-dev @traceloop/jest-opentelemetry 52 | ``` 53 | 54 | Then, you can start testing your microservices: 55 | 56 | ```js 57 | const traceloop = new TraceLoop(); 58 | 59 | await traceloop.axiosInstance.post('http://my.awesome.website/orders/create'); 60 | await traceloop.fetchTraces(); 61 | 62 | expectTrace(traceloop.serviceByName('emails-service')) 63 | .toReceiveHttpRequest() 64 | .ofMethod('POST') 65 | .withBody({ emailTemplate: 'orderCreated', itemId: '123' }); 66 | ``` 67 | 68 | More info can be found in our [docs](https://traceloop.com/docs/jest-otel/getting-started). 69 | 70 | ## What can you test? 71 | 72 | Jest OpenTelemetry can be used to test anything that's happening in your system. 73 | We're constantly adding more, and you're welcome to [suggest yours](https://github.com/traceloop/jest-opentelemetry/issues). 74 | 75 | ### Service Assertions 76 | 77 | - [x] [REST](http://traceloop.com/docs/jest-otel/syntax/services-rest) 78 | - [ ] GraphQL 79 | - [x] [GRPC](http://traceloop.com/docs/jest-otel/syntax/services-grpc) 80 | 81 | ### Database Assertions 82 | 83 | - [x] [PostgreSQL](http://traceloop.com/docs/jest-otel/syntax/db-pg) 84 | - [ ] MongoDB 85 | - [x] [Redis](http://traceloop.com/docs/jest-otel/syntax/db-redis) 86 | - [ ] S3 87 | 88 | ### Analytics Reporting Assertions 89 | 90 | - [ ] Segment 91 | - [ ] Snowflake 92 | - [ ] BigQuery 93 | - [ ] Posthog 94 | 95 | ### External Systems Assertions 96 | 97 | - [ ] SendGrid 98 | - [ ] Stripe 99 | 100 | ## 🌱 Contributing 101 | 102 | Whether it's big or small, we love contributions ❤️ Check out our guide to see how to [get started](https://traceloop.com/docs/contributing/overview). 103 | 104 | Not sure where to get started? You can: 105 | 106 | - [Book a free pairing session with one of our teammates](mailto:nir@traceloop.com?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)! 107 | - Join our Slack, and ask us any questions there. 108 | 109 | ## 💚 Community & Support 110 | 111 | - [Slack](https://join.slack.com/t/traceloopcommunity/shared_invite/zt-1plpfpm6r-zOHKI028VkpcWdobX65C~g) (For live discussion with the community and the Traceloop team) 112 | - [GitHub Discussions](https://github.com/traceloop/jest-opentelemetry/discussions) (For help with building and deeper conversations about features) 113 | - [GitHub Issues](https://github.com/traceloop/jest-opentelemetry/issues) (For any bugs and errors you encounter using Jest OpenTelemetry) 114 | - [Twitter](https://twitter.com/traceloopdev) (Get news fast) 115 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security 2 | 3 | Contact: security@traceloop.com 4 | 5 | Based on [https://supabase.com/.well-known/security.txt](https://supabase.com/.well-known/security.txt) 6 | 7 | We place a high priority on the security of our systems at Traceloop. However, no matter how hard we try to make our systems secure, vulnerabilities can still exist. 8 | 9 | In the event that you discover a vulnerability, please let us know so we can address it as soon as possible. We would like to ask you to help us better protect our clients and our systems. 10 | 11 | ## Out of scope vulnerabilities: 12 | 13 | - Clickjacking on pages with no sensitive actions. 14 | 15 | - Unauthenticated/logout/login CSRF. 16 | 17 | - Attacks requiring MITM or physical access to a user's device. 18 | 19 | - Any activity that could lead to the disruption of our service (DoS). 20 | 21 | - Content spoofing and text injection issues without showing an attack vector/without being able to modify HTML/CSS. 22 | 23 | - Email spoofing 24 | 25 | - Missing DNSSEC, CAA, CSP headers 26 | 27 | - Lack of Secure or HTTP only flag on non-sensitive cookies 28 | 29 | - Deadlinks 30 | 31 | ## Please do the following: 32 | 33 | - E-mail your findings to [security@traceloop.dev](mailto:security@traceloop.dev). 34 | 35 | - Do not run automated scanners on our infrastructure or dashboard. If you wish to do this, contact us and we will set up a sandbox for you. 36 | 37 | - Do not take advantage of the vulnerability or problem you have discovered, for example by downloading more data than necessary to demonstrate the vulnerability or deleting or modifying other people's data, 38 | 39 | - Do not reveal the problem to others until it has been resolved, 40 | 41 | - Do not use attacks on physical security, social engineering, distributed denial of service, spam or applications of third parties, 42 | 43 | - Do provide sufficient information to reproduce the problem, so we will be able to resolve it as quickly as possible. Usually, the IP address or the URL of the affected system and a description of the vulnerability will be sufficient, but complex vulnerabilities may require further explanation. 44 | 45 | ## What we promise: 46 | 47 | - We will respond to your report within 3 business days with our evaluation of the report and an expected resolution date, 48 | 49 | - If you have followed the instructions above, we will not take any legal action against you in regard to the report, 50 | 51 | - We will handle your report with strict confidentiality, and not pass on your personal details to third parties without your permission, 52 | 53 | - We will keep you informed of the progress towards resolving the problem, 54 | 55 | - In the public information concerning the problem reported, we will give your name as the discoverer of the problem (unless you desire otherwise), and 56 | 57 | - We strive to resolve all problems as quickly as possible, and we would like to play an active role in the ultimate publication on the problem after it is resolved. 58 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] }; 2 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Mintlify Starter Kit 2 | 3 | Click on `Use this template` to quickstarter your documentation setup with Mintlify. The starter kit contains examples including 4 | 5 | - Guide pages 6 | - Navigation 7 | - Customizations 8 | - API Reference pages 9 | - Use of popular components 10 | 11 | ### 👩‍💻 Development 12 | 13 | Install the [Mintlify CLI](https://www.npmjs.com/package/mintlify) to preview the documentation changes locally. To install, use the following command 14 | 15 | ``` 16 | npm i mintlify -g 17 | ``` 18 | 19 | Run the following command at the root of your documentation (where mint.json is) 20 | 21 | ``` 22 | mintlify dev 23 | ``` 24 | 25 | ### 😎 Publishing Changes 26 | 27 | Changes will be deployed to production automatically after pushing to the default branch. 28 | 29 | You can also preview changes using PRs, which generates a preview link of the docs. 30 | 31 | #### Troubleshooting 32 | 33 | - Mintlify dev isn't running - Run `mintlify install` it'll re-install dependencies. 34 | - Mintlify dev is updating really slowly - Run `mintlify clear` to clear the cache. 35 | -------------------------------------------------------------------------------- /docs/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traceloop/jest-opentelemetry/fea70d0719085318e9a4468a1d7616e51148dfab/docs/favicon.png -------------------------------------------------------------------------------- /docs/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Introduction' 3 | description: 'Auto-generate code that improves your system reliability' 4 | --- 5 | 6 | Traceloop automatically generates integration tests, alerts, and canary monitoring code based on your OpenTelemetry tracing data. 7 | It uses those traces to learn how your system behaves in normal conditions and automatically generates assertions you 8 | can use to validate that your system working properly. 9 | 10 | These assertions can be used to generate integration tests, alerts, and canary monitoring code. 11 | 12 | Don't worry if you're not familiar with OpenTelemetry. Even if you don't have it currently in your system, you can easily set it up. 13 | 14 | And we can help you too! Ping us at dev@traceloop.dev 15 | 16 | ### Languages 17 | 18 | 19 | 24 | Available 25 | 26 | 27 | In Development 28 | 29 | 30 | In Development 31 | 32 | 33 | -------------------------------------------------------------------------------- /docs/jest-otel/architecture.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Architecture' 3 | description: 'Understand how Jest OpenTelemetry works' 4 | --- 5 | 6 | When you run a test, you will usually send a request to one of the services in your cluster and then monitor how they behave. 7 | To do that, we use the data outputted by OpenTelemetry. 8 | 9 | This data is collected by an OpenTelemetry instance called [Collector](https://opentelemetry.io/docs/collector/). 10 | We connect to it by adding a new standard HTTP exporter (called `otlphttp`) within the collector. 11 | 12 | 13 | Collectors are built of 3 components: 14 | 15 | * Receivers: Receives the data from the instrumented application 16 | * Processors: Processes the data (e.g. filters, enriches, etc.) 17 | * Exporters: Exports the data to a backend (e.g. Jaeger, Zipkin, etc.) 18 | 19 | A standard container deployment of a collector exists where all of this is [configured in a YAML file](https://opentelemetry.io/docs/collector/configuration). 20 | Basically adding the HTTP exporter for test is merely a configuration update. 21 | 22 | 23 | 24 | When Jest starts up, we starts another service called `otel-receiver` that receives the telemetries outputted from the `otlphttp` exporter. 25 | It caches them all locally in memory. Then, when the actual test is ran, each of your assertions are tested against these cached traces. 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/jest-otel/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traceloop/jest-opentelemetry/fea70d0719085318e9a4468a1d7616e51148dfab/docs/jest-otel/architecture.png -------------------------------------------------------------------------------- /docs/jest-otel/contributing/local-env.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Setting up a local developement environment' 3 | --- 4 | 5 | Jest OpenTelemetry is using a monorepo (powered by [Lerna](https://lerna.js.org/)). 6 | 7 | Follow these instructions to set up a local developement environment. 8 | 9 | 1. Fork out repository and clone it locally. 10 | 11 | ```bash 12 | git clone 13 | cd jest-opentelemetry 14 | nvm install 15 | npm install 16 | ``` 17 | 18 | 2. Build the packages: 19 | 20 | ```bash 21 | npm run build 22 | ``` 23 | 24 | 3. Do any changes you want to do. Make sure to add proper tests, especially if you add new assertions. 25 | 26 | 4. Re-build the project (this is needed because the assertion package is using itself in the tests): 27 | 28 | ```bash 29 | npm run build 30 | ``` 31 | 32 | 5. Install a local Postgres database. On mac, you can use [Postgres.app](https://postgresapp.com/). 33 | 34 | 6. Run the [test servers](test-servers). You can either run them locally in a separate terminal or on a local Kubernetes cluster. 35 | These are the servers that we use to test the different assertions we have in Jest OpenTelemetry. 36 | 37 | 7. Run the tests to make sure everything still works: 38 | 39 | ```bash 40 | npm run test 41 | ``` 42 | 43 | 8. Commit, push and open a pull request. 44 | 45 | If you have any questions, hit us up over at [Slack](https://join.slack.com/t/traceloopcommunity/shared_invite/zt-1plpfpm6r-zOHKI028VkpcWdobX65C~g)! 46 | -------------------------------------------------------------------------------- /docs/jest-otel/contributing/overview.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Overview' 3 | description: 'We welcome any contributions to Jest OpenTelemetry, big or small.' 4 | --- 5 | 6 | ## Community 7 | 8 | It's the early days of our project and we're working hard to build an awesome, inclusive community. In order to grow this, all community members must adhere to our [Code of Conduct](https://github.com/traceloop/jest-opentelemetry/blob/main/CODE_OF_CONDUCT.md). 9 | 10 | ## Bugs and issues 11 | 12 | Bug reports help make Jest OpenTelemetry a better experience for everyone. When you report a bug, a template will be created automatically containing information we'd like to know. 13 | 14 | Before raising a new issue, please search existing ones to make sure you're not creating a duplicate. 15 | 16 | 17 | If the issue is related to security, please email us directly at 18 | dev@traceloop.dev. 19 | 20 | 21 | ## Deciding what to work on 22 | 23 | You can start by browsing through our list of issues or adding your own that improves on the test suite experience. Once you've decided on an issue, leave a comment and wait to get approved; this helps avoid multiple people working on the same issue. 24 | 25 | If you're ever in doubt about whether or not a proposed feature aligns with Enrolla as a whole, feel free to raise an issue about it and we'll get back to you promptly. 26 | 27 | ## Writing and submitting code 28 | 29 | Anyone can contribute code to Jest OpenTelemetry. To get started, check out the local development guide, make your changes, and submit a pull request to the main repository. 30 | 31 | ## Licensing 32 | 33 | All of Jest OpenTelemetry's code is under the Apache 2.0 license. 34 | 35 | Any third party components incorporated into our code are licensed under the original license provided by the applicable component owner. 36 | -------------------------------------------------------------------------------- /docs/jest-otel/contributing/test-servers.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Running Test Servers' 3 | --- 4 | 5 | Currently, we have two test servers which are ran and tested together as part of Jest OpenTelemetry internal test suite: 6 | 7 | - **orders-service** - a simple service that creates orders and send emails to users when an order is created. 8 | - **emails-service** - a simple service that sends emails to users. 9 | 10 | ## Running locally 11 | 12 | Running our test servers locally is as simple as running the following command: 13 | 14 | ```bash 15 | npm run start:test-servers 16 | ``` 17 | 18 | ## Running on a local Kubernetes cluster 19 | 20 | 1. Make sure you have a K8s cluster running locally. 21 | If you don't, you can use either [minikube](https://kubernetes.io/docs/tasks/tools/install-minikube/) 22 | or [Docker Desktop](https://www.docker.com/products/docker-desktop) to run a local cluster. 23 | 24 | 1. Build the docker image. 25 | 26 | ```bash 27 | npm run docker:test-servers 28 | ``` 29 | 30 | 1. Install cert-manager (needed by OpenTelemetry operator) 31 | 32 | ```bash 33 | kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.yaml 34 | ``` 35 | 36 | 1. Install the OpenTelemetry operator so that metrics will be generated and collected automatically. 37 | 38 | ```bash 39 | kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml 40 | ``` 41 | 42 | 1. Apply the K8s spec. 43 | 44 | ```bash 45 | kubectl apply -f packages/test-servers/deployment.yaml 46 | ``` 47 | 48 | 49 | If you ever re-built the docker image, make sure to delete the old 50 | deployment first. Run ```kubectl delete -f 51 | packages/test-servers/deployment.yaml ``` 52 | 53 | -------------------------------------------------------------------------------- /docs/jest-otel/getting-started.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Getting Started' 3 | --- 4 | 5 | Jest OpenTelemetry requires that you have OpenTelemetry installed and configured in your microservices. 6 | [Follow the instructions](no-otel) to get it done in 5 minutes without any code changes! 7 | 8 | Feel free to contact us at dev@traceloop.dev if you need any help. 9 | 10 | ## Installing Jest OpenTelemetry 11 | 12 | 1. In your repo, run: 13 | 14 | ```bash 15 | npm install --save-dev @traceloop/jest-opentelemetry jest 16 | ``` 17 | 18 | 2. Update or create your jest configuration (`jest.config.js` or `jest.config.ts`): 19 | 20 | ```js 21 | module.exports = { 22 | preset: '@traceloop/jest-opentelemetry', 23 | }; 24 | ``` 25 | 26 | 3. Start writing tests following our [syntax](syntax/introduction). 27 | 28 | ## Typescript configuration 29 | 30 | Make sure you use `@jest/globals` for your types and not `@types/jest` which is out of date. 31 | See more info [here](https://jestjs.io/docs/getting-started#type-definitions). 32 | -------------------------------------------------------------------------------- /docs/jest-otel/introduction.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Introduction' 3 | --- 4 | 5 | Jest OpenTelemetry is an engine for writing end to end tests for distributed systems with 10 lines of code. 6 | 7 | ```js 8 | const traceloop = new TraceLoop(); 9 | 10 | await traceloop.axiosInstance.post('http://my.awesome.website/orders/create'); 11 | await traceloop.fetchTraces(); 12 | 13 | expectTrace(traceloop.serviceByName('emails-service')) 14 | .toReceiveHttpRequest() 15 | .withMethod('POST') 16 | .withBody({ emailTemplate: 'orderCreated', itemId: '123' }); 17 | ``` 18 | 19 | Everything is running locally on your system and can connect to services running locally on your machine, 20 | or any kubernetes cluster (locally or remotely). 21 | 22 | 23 | We're working on a cloud runner that will enable you to run tests on large 24 | clusters with significant load. [Sign up for our private beta 25 | here](https://c9nshtdss2i.typeform.com/to/mLMW70eV). 26 | 27 | 28 | ## Getting Started 29 | 30 | Select from the following guides to learn more about how to use Jest OpenTelemetry: 31 | 32 | 33 | 38 | Set up Jest OpenTelemetry in your project 39 | 40 | 41 | How to write tests with Jest OpenTelemetry 42 | 43 | 48 | Understand how Jest OpenTelemetry works 49 | 50 | 55 | See our GitHub repo 56 | 57 | 58 | -------------------------------------------------------------------------------- /docs/jest-otel/no-otel.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: "What if I don't have OpenTelemetry installed?" 3 | --- 4 | 5 | No worries! You can install it in any existing Kubernetes environment in under 5 minutes by running the following set of commands. 6 | **No code changes are needed**. 7 | 8 | Feel free to contact us at dev@traceloop.dev if you need any help. 9 | 10 | 11 | [Odigos](https://docs.odigos.io/intro) is a new open source project that can do this for you without a single line of code. 12 | 13 | You can also find more ways to instrument OpenTelemetry on their [documentation](https://opentelemetry.io/docs/js/getting-started/). 14 | 15 | 16 | 17 | 1. Install cert-manager. This is required for the OpenTelemetry operator to work. 18 | 19 | ```bash 20 | kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.11.0/cert-manager.yaml 21 | ``` 22 | 23 | 2. Install the [OpenTelemetry operator](https://opentelemetry.io/docs/k8s-operator/) so that metrics will be generated and collected automatically. 24 | 25 | ```bash 26 | kubectl apply -f https://github.com/open-telemetry/opentelemetry-operator/releases/latest/download/opentelemetry-operator.yaml 27 | ``` 28 | 29 | 3. Create a config file named `otel-collector.yaml` for your OpenTelemetry configurations: 30 | 31 | ```yaml 32 | apiVersion: opentelemetry.io/v1alpha1 33 | kind: Instrumentation 34 | metadata: 35 | name: otel-instrumentation 36 | spec: 37 | nodejs: 38 | image: traceloop/instrument-opentelemetry:node-0.3.0 39 | exporter: 40 | endpoint: http://otel-collector:4317 41 | propagators: 42 | - tracecontext 43 | - baggage 44 | - b3 45 | 46 | --- 47 | apiVersion: opentelemetry.io/v1alpha1 48 | kind: OpenTelemetryCollector 49 | metadata: 50 | name: otel 51 | spec: 52 | config: | 53 | receivers: 54 | otlp: 55 | protocols: 56 | grpc: 57 | http: 58 | processors: 59 | batch: 60 | timeout: 100ms 61 | 62 | exporters: 63 | otlphttp: 64 | endpoint: http://host.docker.internal:4123 65 | 66 | service: 67 | pipelines: 68 | traces: 69 | receivers: [otlp] 70 | processors: [batch] 71 | exporters: [otlphttp] 72 | ``` 73 | 74 | 75 | 76 | We configure 2 separate things: 77 | 78 | 1. The Instrumentation, which is an init-container which will run on any pod you explictly mark (see step 5). 79 | We are using our own init-container to get more data about your application (like the body of HTTP requests). 80 | You can always remove these lines to use the standard open telemetry init-container: 81 | 82 | ```yaml 83 | nodejs: 84 | image: traceloop/instrument-opentelemetry:node-0.3.0 85 | ``` 86 | 87 | 2. The OpenTelemetry collector, which will collect the metrics from the init-container and send them to the test runner. 88 | What's amazing here is that you can add other exporters to this config file to send the metrics to other services like Datadog and others. 89 | 90 | 91 | 92 | 4. Apply the config file: 93 | 94 | ```bash 95 | kubectl apply -f otel-collector.yaml 96 | ``` 97 | 98 | 5. Update any service you want to instrument with the following annotations (Change the 2 occurances of `my-service` to the name of your service): 99 | 100 | 101 | We add an env var named `SERVICE_NAME` to your service so that you can 102 | later identify it in the tests. 103 | 104 | 105 | ```yaml 106 | apiVersion: apps/v1 107 | kind: Deployment 108 | metadata: 109 | name: my-service 110 | spec: 111 | replicas: 1 112 | template: 113 | annotations: 114 | instrumentation.opentelemetry.io/inject-nodejs: 'true' 115 | spec: 116 | containers: 117 | env: 118 | - name: SERVICE_NAME 119 | value: 'my-service' 120 | ``` 121 | 122 | This will automatically instrument your service with OpenTelemetry and send the metrics to the OpenTelemetry collector. 123 | Apply those changes and you're done! You can start writing end to end tests. 124 | -------------------------------------------------------------------------------- /docs/jest-otel/syntax/db-pg.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'PostgreSQL' 3 | --- 4 | 5 | Each assertion should start with defining the service that's sending requests to the Database you want to check. You do this by: 6 | 7 | ```js 8 | expectTrace(traceloop.serviceByName('service-name'))... 9 | ``` 10 | 11 | These are the options you can use for service selection: 12 | 13 | - `serviceByName` - the name of the service as reported in the `service.name` attribute 14 | (automatically reported by [Traceloop's init container](/jest-otel/no-otel)) 15 | - `serviceByK8sPodName` - the name of the pod as reported by Kubernetes 16 | - `serviceByCustomAttribute` - any custom attribute reported by your service to OpenTelemetry 17 | 18 | Following that, you can use any of the following assertions: 19 | 20 | - `toQueryPostgreSQL` 21 | 22 | Then, you can specifically assert for each of the properties of the request: 23 | 24 | - `withDatabaseName` 25 | - `withStatement` 26 | 27 | So, a complete assertion can look like: 28 | 29 | ```js 30 | expectTrace(traceloop.serviceByName('orders-service')) 31 | .toQueryPostgreSQL() 32 | .withDatabaseName('postgres') 33 | .withOperations('INSERT') 34 | .withTables('orders') 35 | .withStatement( 36 | /INSERT INTO orders \(id, price_in_cents\) VALUES \('[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}', [0-9]+\)/, 37 | { compareType: COMPARE_TYPE.REGEX }, 38 | ); 39 | ``` 40 | -------------------------------------------------------------------------------- /docs/jest-otel/syntax/db-redis.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Redis' 3 | --- 4 | 5 | Each assertion should start with defining the service that's sending requests to the Redis Database you want to check. You do this by: 6 | 7 | ```js 8 | expectTrace(traceloop.serviceByName('service-name'))... 9 | ``` 10 | 11 | These are the options you can use for service selection: 12 | 13 | - `serviceByName` - the name of the service as reported in the `service.name` attribute 14 | (automatically reported by [Traceloop's init container](/jest-otel/no-otel)) 15 | - `serviceByK8sPodName` - the name of the pod as reported by Kubernetes 16 | - `serviceByCustomAttribute` - any custom attribute reported by your service to OpenTelemetry 17 | 18 | Following that, you can use any of the following assertions: 19 | 20 | - `toSendRedisCommand` 21 | 22 | Then, you can specifically assert for each of the properties of the request: 23 | 24 | - `withDatabaseName` 25 | - `withStatement` 26 | 27 | So, a complete assertion can look like: 28 | 29 | ```js 30 | expectTrace(traceloop.serviceByName('redis-service')) 31 | .toSendRedisCommend({ times: 2 }) // optional times parameter (defaults to one) 32 | .withDatabaseName('redis-db') 33 | .withStatement( 34 | /^HGET/ 35 | { compareType: COMPARE_TYPE.REGEX }, 36 | ); 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/jest-otel/syntax/overview.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Overview' 3 | --- 4 | 5 | Jest OpenTelemetry syntax is simple and easy to use. 6 | Each test should begin by sending a request to any of your backends in your test environments, like this: 7 | 8 | 9 | Note the use of the `axiosInstance` returned from `traceloop` to send the 10 | request. 11 | 12 | 13 | ```js 14 | const traceloop = new TraceLoop(); 15 | 16 | await traceloop.axiosInstance.post('http://my.awesome.website/orders/create'); 17 | await traceloop.fetchTraces(); 18 | ``` 19 | 20 | Following that, you can run assertions on anything that happened in your system as part of this request. 21 | 22 | ## Service Assertions 23 | 24 | 25 | 26 | Available 27 | 28 | 33 | Available 34 | 35 | 36 | In Development 37 | 38 | 39 | 40 | ## Database Assertions 41 | 42 | 43 | 44 | Available 45 | 46 | 47 | Available 48 | 49 | 50 | In Development 51 | 52 | 53 | In Development 54 | 55 | 56 | 57 | ## Analytics Reporting Assertions 58 | 59 | 60 | 61 | In Development 62 | 63 | 64 | In Development 65 | 66 | 67 | In Development 68 | 69 | 70 | In Development 71 | 72 | 73 | 74 | ## External Systems Assertions 75 | 76 | 77 | 78 | In Development 79 | 80 | 81 | In Development 82 | 83 | 84 | -------------------------------------------------------------------------------- /docs/jest-otel/syntax/services-grpc.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'GRPC Assertions' 3 | --- 4 | 5 | Each assertion should start with defining the service you want to check. You do this by: 6 | 7 | ```js 8 | expectTrace(traceloop.serviceByName('service-name'))... 9 | ``` 10 | 11 | These are the options you can use for service selection: 12 | 13 | - `serviceByName` - the name of the service as reported in the `service.name` attribute 14 | (automatically reported by [Traceloop's init container](/jest-otel/no-otel)) 15 | - `serviceByK8sPodName` - the name of the pod as reported by Kubernetes 16 | - `serviceByCustomAttribute` - any custom attribute reported by your service to OpenTelemetry 17 | 18 | Following that, you can use any of the following assertions: 19 | 20 | - `toSendGrpcRequest` 21 | - `toReceiveGrpcRequest` 22 | 23 | Then, you can specifically assert for each of the properties of the request: 24 | 25 | - `withRpcMethod` 26 | - `withRpcService` 27 | - `withRpcGrpcStatusCode` 28 | - `withNetPeerName` 29 | - `withHostName` 30 | 31 | So, a complete assertion can look like: 32 | 33 | ```js 34 | expectTrace(traceloop.serviceByName('users-service')) 35 | .toReceiveGrpcRequest() 36 | .withRpcMethod('CreateUser') 37 | .withRpcGrpcStatusCode(0); 38 | ``` 39 | -------------------------------------------------------------------------------- /docs/jest-otel/syntax/services-rest.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'REST Assertions' 3 | --- 4 | 5 | Each assertion should start with defining the service you want to check. You do this by: 6 | 7 | ```js 8 | expectTrace(traceloop.serviceByName('service-name'))... 9 | ``` 10 | 11 | These are the options you can use for service selection: 12 | 13 | - `serviceByName` - the name of the service as reported in the `service.name` attribute 14 | (automatically reported by [Traceloop's init container](/jest-otel/no-otel)) 15 | - `serviceByK8sPodName` - the name of the pod as reported by Kubernetes 16 | - `serviceByCustomAttribute` - any custom attribute reported by your service to OpenTelemetry 17 | 18 | Following that, you can use any of the following assertions: 19 | 20 | - `toSendHttpRequest` 21 | - `toReceiveHttpRequest` 22 | 23 | Then, you can specifically assert for each of the properties of the request: 24 | 25 | - `withBody` 26 | - `withHeader` 27 | - `withMethod` 28 | - `withStatusCode` 29 | 30 | So, a complete assertion can look like: 31 | 32 | ```js 33 | expectTrace(traceloop.serviceByName('users-service')) 34 | .toReceiveHttpRequest() 35 | .ofMethod('POST') 36 | .withHeader('Content-Type', 'application/json') 37 | .withRequestBody({ 38 | id: 1, 39 | name: 'John Doe', 40 | address: '123 Main St', 41 | }); 42 | ``` 43 | -------------------------------------------------------------------------------- /docs/mint.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "traceloop", 3 | "favicon": "/favicon.png", 4 | "logo": { 5 | "light": "/logo/light.svg", 6 | "dark": "/logo/dark.svg" 7 | }, 8 | "colors": { 9 | "primary": "#FFB53D", 10 | "light": "#FFF238", 11 | "dark": "#FF3D5D", 12 | "anchors": { 13 | "from": "#FFF238", 14 | "to": "#FF3D5D" 15 | }, 16 | "background": { 17 | "dark": "#121212" 18 | } 19 | }, 20 | "topbarLinks": [ 21 | { 22 | "name": "Website", 23 | "url": "https://www.traceloop.dev" 24 | } 25 | ], 26 | "topbarCtaButton": { 27 | "type": "github", 28 | "url": "https://github.com/traceloop/jest-opentelemetry" 29 | }, 30 | "anchors": [ 31 | { 32 | "name": "Jest OpenTelemetry", 33 | "icon": "node", 34 | "url": "jest-otel" 35 | }, 36 | { 37 | "name": "Community", 38 | "icon": "slack", 39 | "url": "https://join.slack.com/t/traceloopcommunity/shared_invite/zt-1plpfpm6r-zOHKI028VkpcWdobX65C~g" 40 | }, 41 | { 42 | "name": "GitHub", 43 | "icon": "github", 44 | "url": "https://github.com/traceloop" 45 | } 46 | ], 47 | "navigation": [ 48 | { 49 | "group": "Learn", 50 | "pages": ["introduction"] 51 | }, 52 | { 53 | "group": "Quick Start", 54 | "pages": [ 55 | "jest-otel/introduction", 56 | "jest-otel/getting-started", 57 | "jest-otel/no-otel", 58 | "jest-otel/architecture" 59 | ] 60 | }, 61 | { 62 | "group": "Syntax", 63 | "pages": [ 64 | "jest-otel/syntax/overview", 65 | "jest-otel/syntax/services-rest", 66 | "jest-otel/syntax/services-grpc", 67 | "jest-otel/syntax/db-pg", 68 | "jest-otel/syntax/db-redis" 69 | ] 70 | }, 71 | { 72 | "group": "Contributing", 73 | "pages": [ 74 | "jest-otel/contributing/overview", 75 | "jest-otel/contributing/local-env", 76 | "jest-otel/contributing/test-servers" 77 | ] 78 | } 79 | ], 80 | "footerSocials": { 81 | "github": "https://github.com/traceloop", 82 | "twitter": "https://twitter.com/traceloopdev" 83 | }, 84 | "analytics": { 85 | "posthog": { 86 | "apiKey": "phc_S07ERHfM7mBYvjzzW18gxfTjhly2dOD8y2WiRYajV5J" 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /img/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traceloop/jest-opentelemetry/fea70d0719085318e9a4468a1d7616e51148dfab/img/logo.png -------------------------------------------------------------------------------- /jest-opentelemetry.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | timeout: 2000, 3 | useLocalOtelReceiver: true, // default (false) is to use traceloop backend 4 | }; 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: ['/packages/*'], 3 | }; 4 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "useWorkspaces": true, 4 | "version": "0.8.0" 5 | } 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jest-opentelemetry", 3 | "private": true, 4 | "scripts": { 5 | "build": "lerna run build", 6 | "format": "prettier --write .", 7 | "lint": "prettier --check . && eslint .", 8 | "start:otel-receiver": "lerna run --scope @traceloop/otel-receiver start", 9 | "start:test-servers": "lerna run --scope @traceloop/test-servers start", 10 | "docker:instrument-opentelemetry": "docker build -f packages/instrument-opentelemetry/Dockerfile . -t instrument-opentelemetry", 11 | "docker:test-servers": "docker build -f packages/test-servers/Dockerfile . -t test-servers", 12 | "test": "TRACELOOP_URL='http://localhost:4123/v1/traces' jest", 13 | "test-ci": "TRACELOOP_URL='http://localhost:4123/v1/traces' concurrently -k --success \"command-1\" --hide 0 \"npm:start:test-servers\" \"npm:test\"", 14 | "release": "npm run build && lerna publish --conventional-commits --no-private" 15 | }, 16 | "workspaces": [ 17 | "packages/*" 18 | ], 19 | "devDependencies": { 20 | "@commitlint/cli": "^17.4.4", 21 | "@commitlint/config-conventional": "^17.4.4", 22 | "@rollup/plugin-typescript": "^11.0.0", 23 | "@tsconfig/recommended": "^1.0.2", 24 | "@typescript-eslint/eslint-plugin": "^5.54.1", 25 | "@typescript-eslint/parser": "^5.52.0", 26 | "concurrently": "^7.6.0", 27 | "eslint": "^8.35.0", 28 | "husky": "^8.0.3", 29 | "jest": "^29.4.3", 30 | "lerna": "^6.5.1", 31 | "typescript": "^4.9.5" 32 | }, 33 | "dependencies": { 34 | "@swc/jest": "^0.2.24", 35 | "eslint-config-prettier": "^8.8.0", 36 | "prettier": "^2.8.4" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/dist/**/*.js 3 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/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 [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/README.md: -------------------------------------------------------------------------------- 1 | # expect-opentelemetry 2 | 3 | Assertion library for OpenTelemetry. 4 | 5 | ``` 6 | npm install @traceloop/expect-opentelemetry 7 | ``` 8 | 9 | ## Usage 10 | 11 | ## License 12 | 13 | Apache-2.0 14 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '/../jest-opentelemetry', 3 | transform: { 4 | '^.+\\.(t|j)sx?$': ['@swc/jest'], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@traceloop/expect-opentelemetry", 3 | "description": "Assertion toolkit for OpenTelemetry tests.", 4 | "version": "0.8.0", 5 | "license": "Apache-2.0", 6 | "type": "commonjs", 7 | "main": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/traceloop/jest-opentelemetry.git", 12 | "directory": "packages/expect-opentelemetry" 13 | }, 14 | "homepage": "https://github.com/traceloop/jest-opentelemetry/tree/main/packages/expect-opentelemetry#readme", 15 | "bugs": { 16 | "url": "https://github.com/traceloop/jest-opentelemetry/issues" 17 | }, 18 | "engines": { 19 | "node": ">=14.0.0" 20 | }, 21 | "keywords": [ 22 | "jest", 23 | "opentelemetry", 24 | "jest-opentelemetry", 25 | "expect", 26 | "assert", 27 | "should", 28 | "assertion" 29 | ], 30 | "publishConfig": { 31 | "access": "public" 32 | }, 33 | "scripts": { 34 | "prebuild": "rm -rf dist", 35 | "build": "rollup -c" 36 | }, 37 | "dependencies": { 38 | "@opentelemetry/semantic-conventions": "^1.10.1", 39 | "@traceloop/otel-proto": "^0.8.0", 40 | "axios": "^1.3.4", 41 | "deep-equal": "^2.2.0" 42 | }, 43 | "devDependencies": { 44 | "@types/deep-equal": "^1.0.1", 45 | "rollup": "^3.20.0", 46 | "rollup-plugin-swc3": "^0.8.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { swc, defineRollupSwcOption } from 'rollup-plugin-swc3'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | 4 | const bundle = (config) => ({ 5 | external: (id) => { 6 | return !/^[./]/.test(id); 7 | }, 8 | ...config, 9 | }); 10 | 11 | const swcPlugin = swc( 12 | defineRollupSwcOption({ 13 | jsc: { target: 'es2021' }, 14 | }), 15 | ); 16 | 17 | export default [ 18 | bundle({ 19 | input: 'src/index.ts', 20 | output: { 21 | file: 'dist/index.js', 22 | format: 'cjs', 23 | }, 24 | plugins: [ 25 | swcPlugin, 26 | typescript({ 27 | tsconfig: '../../tsconfig.json', 28 | exclude: ['**/*.test.ts'], 29 | }), 30 | ], 31 | }), 32 | ]; 33 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | toReceiveHttpRequest, 3 | toSendHttpRequest, 4 | toQueryPostgreSQL, 5 | toReceiveGrpcRequest, 6 | toSendGrpcRequest, 7 | toSendRedisCommand, 8 | } from './matchers/service'; 9 | import { expect } from '@jest/globals'; 10 | import { 11 | GrpcRequest, 12 | HttpRequest, 13 | PostgreSQLQuery, 14 | RedisCommand, 15 | Service, 16 | } from './resources'; 17 | export { setDefaultOptions, getDefaultOptions } from './options'; 18 | 19 | export * from './matchers'; 20 | export * from './resources'; 21 | export * from './trace-loop'; 22 | 23 | const serviceMatchers = { 24 | toReceiveHttpRequest, 25 | toSendHttpRequest, 26 | toQueryPostgreSQL, 27 | toReceiveGrpcRequest, 28 | toSendGrpcRequest, 29 | toSendRedisCommand, 30 | }; 31 | 32 | interface MatcherOptions { 33 | times: number; 34 | } 35 | 36 | interface TraceMatchers { 37 | toReceiveHttpRequest(): HttpRequest; 38 | toSendHttpRequest(): HttpRequest; 39 | toQueryPostgreSQL(options?: MatcherOptions): PostgreSQLQuery; 40 | toReceiveGrpcRequest(): GrpcRequest; 41 | toSendGrpcRequest(): GrpcRequest; 42 | toSendRedisCommand(options?: MatcherOptions): RedisCommand; 43 | } 44 | 45 | function createMatcher(matcher, type) { 46 | return function throwingMatcher(...args) { 47 | if (typeof expect !== 'undefined') { 48 | expect.getState().assertionCalls += 1; 49 | } 50 | 51 | try { 52 | return matcher(type, ...args); 53 | } catch (error: any) { 54 | Error.captureStackTrace(error, throwingMatcher); 55 | throw error; 56 | } 57 | }; 58 | } 59 | 60 | export function expectTrace(actual: Service): TraceMatchers { 61 | const expectation: Partial = {}; 62 | Object.keys(serviceMatchers).forEach((key) => { 63 | if (key === 'not') return; 64 | expectation[key] = createMatcher(serviceMatchers[key], actual); 65 | }); 66 | 67 | return expectation as TraceMatchers; 68 | } 69 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './service'; 2 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/service/grpc-request.test.ts: -------------------------------------------------------------------------------- 1 | import { jest, describe, it, beforeAll } from '@jest/globals'; 2 | import { expectTrace } from '../..'; 3 | import { TraceLoop } from '../../trace-loop'; 4 | 5 | jest.setTimeout(30000); 6 | 7 | describe('grpc request matchers', () => { 8 | describe('when orders-service makes a gRPC call to the grpc server', () => { 9 | let traceloop: TraceLoop; 10 | beforeAll(async () => { 11 | traceloop = new TraceLoop(); 12 | 13 | await traceloop.axiosInstance.post('http://localhost:3000/orders/create'); 14 | await traceloop.fetchTraces(); 15 | }); 16 | 17 | it('should contain outbound gRPC call from orders-service', async () => { 18 | expectTrace( 19 | traceloop.serviceByName('orders-service'), 20 | ).toSendGrpcRequest(); 21 | }); 22 | 23 | it('should contain inbound gRPC call to grpc service', async () => { 24 | expectTrace( 25 | traceloop.serviceByName('grpc-service'), 26 | ).toReceiveGrpcRequest(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/service/http-request.test.ts: -------------------------------------------------------------------------------- 1 | import { jest, describe, it, beforeAll } from '@jest/globals'; 2 | import { expectTrace } from '../..'; 3 | import { TraceLoop } from '../../trace-loop'; 4 | 5 | jest.setTimeout(30000); 6 | 7 | describe('http request matchers', () => { 8 | describe('when orders-service makes an http call to emails-service', () => { 9 | let traceloop: TraceLoop; 10 | beforeAll(async () => { 11 | traceloop = new TraceLoop(); 12 | 13 | await traceloop.axiosInstance.post('http://localhost:3000/orders/create'); 14 | await traceloop.fetchTraces(); 15 | }); 16 | 17 | it('should contain outbound http call from orders-service', async () => { 18 | expectTrace( 19 | traceloop.serviceByName('orders-service'), 20 | ).toSendHttpRequest(); 21 | }); 22 | 23 | it('should contain inbound http call to emails-service', async () => { 24 | expectTrace( 25 | traceloop.serviceByName('emails-service'), 26 | ).toReceiveHttpRequest(); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/service/index.ts: -------------------------------------------------------------------------------- 1 | export * from './to-receive-http-request'; 2 | export * from './to-send-http-request'; 3 | export * from './to-query-postgresql'; 4 | export * from './to-receive-grpc-request'; 5 | export * from './to-send-grpc-request'; 6 | export * from './to-send-redis-command'; 7 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/service/to-query-postgresql.test.ts: -------------------------------------------------------------------------------- 1 | import { jest, describe, it, beforeAll } from '@jest/globals'; 2 | import { expectTrace } from '../..'; 3 | import { TraceLoop } from '../../trace-loop'; 4 | import { COMPARE_TYPE } from '../utils'; 5 | 6 | jest.setTimeout(30000); 7 | 8 | describe('postgresql query', () => { 9 | let traceloop: TraceLoop; 10 | beforeAll(async () => { 11 | traceloop = new TraceLoop(); 12 | 13 | await traceloop.axiosInstance.post('http://localhost:3000/orders/create'); 14 | await traceloop.fetchTraces(); 15 | }); 16 | 17 | it('should see orders-service querying postgresql named postgres', async () => { 18 | expectTrace(traceloop.serviceByName('orders-service')) 19 | .toQueryPostgreSQL() 20 | .withDatabaseName('postgres', { compareType: COMPARE_TYPE.EQUALS }); 21 | }); 22 | 23 | it('should see orders-service querying postgresql named postgres and inserting an order with uuid as id and integer price_in_cents', async () => { 24 | expectTrace(traceloop.serviceByName('orders-service')) 25 | .toQueryPostgreSQL() 26 | .withDatabaseName('postgres', { compareType: COMPARE_TYPE.EQUALS }) 27 | .withOperations('INSERT') 28 | .withTables('orders') 29 | .withStatement( 30 | /INSERT INTO orders \(id, price_in_cents\) VALUES \('[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}', [0-9]+\)/, 31 | { compareType: COMPARE_TYPE.REGEX }, 32 | ); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/service/to-query-postgresql.ts: -------------------------------------------------------------------------------- 1 | import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 2 | import { Service } from '../../resources/service'; 3 | import { opentelemetry } from '@traceloop/otel-proto'; 4 | import { PostgreSQLQuery } from '../../resources/postgresql-query'; 5 | 6 | export function toQueryPostgreSQL( 7 | service: Service, 8 | options = { times: 1 }, 9 | ): PostgreSQLQuery { 10 | const { name: serviceName, spans } = service; 11 | 12 | const filteredSpans = spans.filter((span) => { 13 | return ( 14 | span.kind === 15 | opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_CLIENT && 16 | span.attributes?.find( 17 | (attribute: opentelemetry.proto.common.v1.IKeyValue) => { 18 | return ( 19 | attribute.key === SemanticAttributes.DB_SYSTEM && 20 | attribute.value?.stringValue === 'postgresql' 21 | ); 22 | }, 23 | ) 24 | ); 25 | }); 26 | 27 | if (filteredSpans.length < options.times) { 28 | throw new Error( 29 | `Expected ${options.times} queries by ${serviceName} to postgresql, but found ${filteredSpans.length}.`, 30 | ); 31 | } 32 | 33 | return new PostgreSQLQuery(filteredSpans, serviceName, options.times); 34 | } 35 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/service/to-receive-grpc-request.ts: -------------------------------------------------------------------------------- 1 | import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 2 | import { Service } from '../../resources/service'; 3 | import { opentelemetry } from '@traceloop/otel-proto'; 4 | import { GrpcRequest } from '../../resources'; 5 | 6 | export function toReceiveGrpcRequest(service: Service): GrpcRequest { 7 | const { name: serviceName, spans } = service; 8 | const spanKind = opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_SERVER; 9 | 10 | const filteredSpans = spans.filter( 11 | (span: opentelemetry.proto.trace.v1.ISpan) => { 12 | return ( 13 | span.kind === spanKind && 14 | span.attributes?.find( 15 | (attribute: opentelemetry.proto.common.v1.IKeyValue) => { 16 | return ( 17 | attribute.key === SemanticAttributes.RPC_SYSTEM && 18 | attribute.value?.stringValue === 'grpc' 19 | ); 20 | }, 21 | ) 22 | ); 23 | }, 24 | ); 25 | 26 | if (filteredSpans.length === 0) { 27 | throw new Error(`No gRPC call was received by ${serviceName}`); 28 | } 29 | 30 | return new GrpcRequest(filteredSpans, { serviceName, spanKind }); 31 | } 32 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/service/to-receive-http-request.ts: -------------------------------------------------------------------------------- 1 | import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 2 | import { HttpRequest } from '../../resources/http-request'; 3 | import { Service } from '../../resources/service'; 4 | import { opentelemetry } from '@traceloop/otel-proto'; 5 | 6 | export function toReceiveHttpRequest(service: Service): HttpRequest { 7 | const { name: serviceName, spans } = service; 8 | const spanKind = opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_SERVER; 9 | 10 | const filteredSpans = spans.filter((span) => { 11 | return ( 12 | span.kind === spanKind && 13 | span.attributes?.find((attribute) => { 14 | return attribute.key === SemanticAttributes.HTTP_METHOD; 15 | }) 16 | ); 17 | }); 18 | 19 | if (filteredSpans.length === 0) { 20 | throw new Error(`No HTTP call received by ${serviceName}`); 21 | } 22 | 23 | return new HttpRequest(filteredSpans, { serviceName, spanKind }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/service/to-send-grpc-request.ts: -------------------------------------------------------------------------------- 1 | import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 2 | import { Service } from '../../resources/service'; 3 | import { opentelemetry } from '@traceloop/otel-proto'; 4 | import { GrpcRequest } from '../../resources'; 5 | 6 | export function toSendGrpcRequest(service: Service): GrpcRequest { 7 | const { name: serviceName, spans } = service; 8 | const spanKind = opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_CLIENT; 9 | 10 | const filteredSpans = spans.filter( 11 | (span: opentelemetry.proto.trace.v1.ISpan) => { 12 | return ( 13 | span.kind === spanKind && 14 | span.attributes?.find( 15 | (attribute: opentelemetry.proto.common.v1.IKeyValue) => { 16 | return ( 17 | attribute.key === SemanticAttributes.RPC_SYSTEM && 18 | attribute.value?.stringValue === 'grpc' 19 | ); 20 | }, 21 | ) 22 | ); 23 | }, 24 | ); 25 | 26 | if (filteredSpans.length === 0) { 27 | throw new Error(`No gRPC call was sent by ${serviceName}`); 28 | } 29 | 30 | return new GrpcRequest(filteredSpans, { serviceName, spanKind }); 31 | } 32 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/service/to-send-http-request.ts: -------------------------------------------------------------------------------- 1 | import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 2 | import { HttpRequest } from '../../resources/http-request'; 3 | import { Service } from '../../resources/service'; 4 | import { opentelemetry } from '@traceloop/otel-proto'; 5 | 6 | export function toSendHttpRequest(service: Service): HttpRequest { 7 | const { name: serviceName, spans } = service; 8 | const spanKind = opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_CLIENT; 9 | 10 | const filteredSpans = spans.filter((span) => { 11 | return ( 12 | span.kind === spanKind && 13 | span.attributes?.find((attribute) => { 14 | return attribute.key === SemanticAttributes.HTTP_METHOD; 15 | }) 16 | ); 17 | }); 18 | 19 | if (filteredSpans.length === 0) { 20 | throw new Error(`No HTTP call was sent by ${serviceName}`); 21 | } 22 | 23 | return new HttpRequest(filteredSpans, { serviceName, spanKind }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/service/to-send-redis-command.ts: -------------------------------------------------------------------------------- 1 | import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 2 | import { opentelemetry } from '@traceloop/otel-proto'; 3 | import { RedisCommand, Service } from '../../resources'; 4 | 5 | export function toSendRedisCommand( 6 | service: Service, 7 | options = { times: 1 }, 8 | ): RedisCommand { 9 | const { name: serviceName, spans } = service; 10 | 11 | const filteredSpans = spans.filter((span) => { 12 | return ( 13 | span.kind === 14 | opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_CLIENT && 15 | span.attributes?.find( 16 | (attribute: opentelemetry.proto.common.v1.IKeyValue) => { 17 | return ( 18 | attribute.key === SemanticAttributes.DB_SYSTEM && 19 | attribute.value?.stringValue === 'redis' 20 | ); 21 | }, 22 | ) 23 | ); 24 | }); 25 | 26 | if (filteredSpans.length < options.times) { 27 | throw new Error( 28 | `Expected ${options.times} queries by ${serviceName} to redis, but found ${filteredSpans.length}.`, 29 | ); 30 | } 31 | 32 | return new RedisCommand(filteredSpans, serviceName, options.times); 33 | } 34 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/utils/comparators.ts: -------------------------------------------------------------------------------- 1 | import deepEqual from 'deep-equal'; 2 | import { expect } from '@jest/globals'; 3 | import { CompareOptions, COMPARE_TYPE } from './compare-types'; 4 | 5 | type MaybeString = string | undefined | null; 6 | 7 | export const stringEquals = (a: MaybeString, b: MaybeString): boolean => 8 | a === b; 9 | 10 | export const stringContains = (a: MaybeString, b: MaybeString): boolean => 11 | !!a && !!b && a.includes(b); 12 | 13 | export const stringByRegex = ( 14 | a: MaybeString, 15 | b: RegExp | undefined | null, 16 | ): boolean => !!a && !!b && b.test(a); 17 | 18 | export const objectEquals = ( 19 | a: Record, 20 | b: Record, 21 | ): boolean => deepEqual(a, b); 22 | 23 | export const objectContains = ( 24 | a: Record, 25 | b: Record, 26 | ): boolean => { 27 | try { 28 | expect(a).toMatchObject(b); 29 | return true; 30 | } catch (e) { 31 | return false; 32 | } 33 | }; 34 | 35 | export const stringCompare = ( 36 | a: MaybeString, 37 | b: MaybeString | RegExp, 38 | options?: CompareOptions, 39 | ): boolean => { 40 | switch (options?.compareType) { 41 | case COMPARE_TYPE.CONTAINS: 42 | return stringContains(a, b as string); 43 | case COMPARE_TYPE.REGEX: 44 | return stringByRegex(a, b as RegExp); 45 | default: 46 | return stringEquals(a, b as string); 47 | } 48 | }; 49 | 50 | export const objectCompare = ( 51 | a: Record, 52 | b: Record, 53 | options?: CompareOptions, 54 | ): boolean => { 55 | switch (options?.compareType) { 56 | case COMPARE_TYPE.CONTAINS: 57 | return objectContains(a, b); 58 | default: 59 | return objectEquals(a, b); 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/utils/compare-types.ts: -------------------------------------------------------------------------------- 1 | export const COMPARE_TYPE = { 2 | EQUALS: 'equals', 3 | CONTAINS: 'contains', 4 | REGEX: 'regex', 5 | } as const; 6 | 7 | export type CompareType = (typeof COMPARE_TYPE)[keyof typeof COMPARE_TYPE]; 8 | 9 | export type CompareOptions = { compareType: CompareType }; 10 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/utils/filters.ts: -------------------------------------------------------------------------------- 1 | import { opentelemetry } from '@traceloop/otel-proto'; 2 | import { objectCompare, stringCompare } from './comparators'; 3 | import { CompareOptions } from './compare-types'; 4 | 5 | export const extractAttributeStringValues = ( 6 | spans: opentelemetry.proto.trace.v1.ISpan[], 7 | expected: string, 8 | ): string[] => { 9 | return spans 10 | .map((span) => { 11 | return span.attributes?.find( 12 | (attribute: opentelemetry.proto.common.v1.IKeyValue) => 13 | attribute.key === expected, 14 | )?.value?.stringValue; 15 | }) 16 | .filter((statement) => !!statement) as string[]; 17 | }; 18 | 19 | export const filterByAttributeKey = ( 20 | spans: opentelemetry.proto.trace.v1.ISpan[], 21 | attName: string, 22 | ) => 23 | spans.filter((span) => { 24 | return span.attributes?.find( 25 | (attribute: opentelemetry.proto.common.v1.IKeyValue) => { 26 | return attribute.key === attName; 27 | }, 28 | ); 29 | }); 30 | 31 | export const filterBySpanKind = ( 32 | spans: opentelemetry.proto.trace.v1.ISpan[], 33 | expected: opentelemetry.proto.trace.v1.Span.SpanKind, 34 | ) => spans.filter((span) => span.kind === expected); 35 | 36 | export const filterByAttributeStringValue = ( 37 | spans: opentelemetry.proto.trace.v1.ISpan[], 38 | attName: string, 39 | attValue: string | RegExp, 40 | options?: CompareOptions, 41 | ) => 42 | spans.filter((span) => { 43 | return span.attributes?.find( 44 | (attribute) => 45 | attribute.key === attName && 46 | stringCompare(attribute.value?.stringValue, attValue, options), 47 | ); 48 | }); 49 | 50 | export const filterByAttributeIntValue = ( 51 | spans: opentelemetry.proto.trace.v1.ISpan[], 52 | attName: string, 53 | attValue: number, 54 | ) => 55 | spans.filter((span) => { 56 | return span.attributes?.find( 57 | (attribute: opentelemetry.proto.common.v1.IKeyValue) => 58 | attribute.key === attName && 59 | attribute.value?.intValue.toNumber() === attValue, 60 | ); 61 | }); 62 | 63 | export const filterByAttributeJSON = ( 64 | spans: opentelemetry.proto.trace.v1.ISpan[], 65 | attName: string, 66 | attValue: Record, 67 | options?: CompareOptions, 68 | ) => 69 | spans.filter((span) => { 70 | try { 71 | const json = JSON.parse( 72 | span.attributes?.find((attribute) => attribute.key === attName)?.value 73 | ?.stringValue || '', 74 | ); 75 | 76 | return objectCompare(json, attValue, options); 77 | } catch (e) { 78 | return false; 79 | } 80 | }); 81 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/matchers/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './compare-types'; 2 | export * from './comparators'; 3 | export * from './filters'; 4 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/options.ts: -------------------------------------------------------------------------------- 1 | let defaultOptionsValue = { timeout: 500 }; 2 | 3 | export const setDefaultOptions = (options: any) => { 4 | defaultOptionsValue = options; 5 | }; 6 | 7 | export const getDefaultOptions = () => { 8 | return defaultOptionsValue; 9 | }; 10 | 11 | export const defaultOptions = (options: any) => ({ 12 | ...getDefaultOptions(), 13 | ...options, 14 | }); 15 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/resources/grpc-request.test.ts: -------------------------------------------------------------------------------- 1 | import { jest, describe, it } from '@jest/globals'; 2 | import { expectTrace } from '../..'; 3 | import { TraceLoop } from '../trace-loop'; 4 | import { GrpcRequest } from '../resources/grpc-request'; 5 | 6 | jest.setTimeout(30000); 7 | 8 | describe('resource grpc request matchers', () => { 9 | describe('when orders-service makes a gRPC call to grpc-service', () => { 10 | let traceloop: TraceLoop; 11 | beforeAll(async () => { 12 | traceloop = new TraceLoop(); 13 | 14 | await traceloop.axiosInstance.post('http://localhost:3000/orders/create'); 15 | await traceloop.fetchTraces(); 16 | }); 17 | 18 | it('should contain outbound grpc call from orders-service with all parameters', async () => { 19 | expectTrace(traceloop.serviceByName('orders-service')) 20 | .toSendGrpcRequest() 21 | .withRpcMethod('SayHello') 22 | .withRpcService('Greeter', { compareType: 'contains' }) 23 | .withRpcGrpcStatusCode(GrpcRequest.GRPC_STATUS_CODE.OK); 24 | }); 25 | 26 | it('should contain inbound gRPC call to grpc-service', async () => { 27 | expectTrace(traceloop.serviceByName('grpc-service')) 28 | .toReceiveGrpcRequest() 29 | .withRpcMethod('SayHello') 30 | .withRpcService('Greeter', { compareType: 'contains' }) 31 | .withRpcGrpcStatusCode(GrpcRequest.GRPC_STATUS_CODE.OK); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/resources/grpc-request.ts: -------------------------------------------------------------------------------- 1 | import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 2 | import { opentelemetry } from '@traceloop/otel-proto'; 3 | import { 4 | CompareOptions, 5 | filterByAttributeIntValue, 6 | filterByAttributeStringValue, 7 | } from '../matchers/utils'; 8 | 9 | const STATUS_CODE = { 10 | OK: 0, 11 | CANCELLED: 1, 12 | UNKNOWN: 2, 13 | INVALID_ARGUMENT: 3, 14 | DEADLINE_EXCEEDED: 4, 15 | NOT_FOUND: 5, 16 | ALREADY_EXISTS: 6, 17 | PERMISSION_DENIED: 7, 18 | RESOURCE_EXHAUSTED: 8, 19 | FAILED_PRECONDITION: 9, 20 | ABORTED: 10, 21 | OUT_OF_RANGE: 11, 22 | UNIMPLEMENTED: 12, 23 | INTERNAL: 13, 24 | UNAVAILABLE: 14, 25 | DATA_LOSS: 15, 26 | UNAUTHENTICATED: 16, 27 | } as const; 28 | 29 | type StatusCode = (typeof STATUS_CODE)[keyof typeof STATUS_CODE]; 30 | 31 | export class GrpcRequest { 32 | static readonly GRPC_STATUS_CODE = STATUS_CODE; 33 | 34 | constructor( 35 | readonly spans: opentelemetry.proto.trace.v1.ISpan[], 36 | readonly extra: { 37 | serviceName: string; 38 | spanKind: opentelemetry.proto.trace.v1.Span.SpanKind; 39 | }, 40 | ) {} 41 | 42 | withRpcMethod(method: string, options?: CompareOptions) { 43 | const filteredSpans = filterByAttributeStringValue( 44 | this.spans, 45 | SemanticAttributes.RPC_METHOD, 46 | method, 47 | options, 48 | ); 49 | 50 | if (filteredSpans.length === 0) { 51 | throw new Error( 52 | `No gRPC call of method ${method} ${this.serviceErrorBySpanKind()}`, 53 | ); 54 | } 55 | 56 | return new GrpcRequest(filteredSpans, this.extra); 57 | } 58 | 59 | withRpcService(service: string, options?: CompareOptions) { 60 | const filteredSpans = filterByAttributeStringValue( 61 | this.spans, 62 | SemanticAttributes.RPC_SERVICE, 63 | service, 64 | options, 65 | ); 66 | 67 | if (filteredSpans.length === 0) { 68 | throw new Error( 69 | `No gRPC call for service ${service} ${this.serviceErrorBySpanKind()}`, 70 | ); 71 | } 72 | 73 | return new GrpcRequest(filteredSpans, this.extra); 74 | } 75 | 76 | withRpcGrpcStatusCode(code: StatusCode) { 77 | const filteredSpans = filterByAttributeIntValue( 78 | this.spans, 79 | SemanticAttributes.RPC_GRPC_STATUS_CODE, 80 | code, 81 | ); 82 | 83 | // spec says it should be an int, but in practice we get strings 84 | const filteredSpansString = filterByAttributeStringValue( 85 | this.spans, 86 | SemanticAttributes.RPC_GRPC_STATUS_CODE, 87 | code.toString(), 88 | ); 89 | 90 | if (filteredSpans.length === 0 && filteredSpansString.length === 0) { 91 | throw new Error( 92 | `No gRPC call with status code ${code} ${this.serviceErrorBySpanKind()}`, 93 | ); 94 | } 95 | 96 | return new GrpcRequest( 97 | filteredSpans.length !== 0 ? filteredSpans : filteredSpansString, 98 | this.extra, 99 | ); 100 | } 101 | 102 | withNetPeerName(netPeerName: string, options?: CompareOptions) { 103 | const filteredSpans = filterByAttributeStringValue( 104 | this.spans, 105 | SemanticAttributes.NET_PEER_NAME, 106 | netPeerName, 107 | options, 108 | ); 109 | 110 | if (filteredSpans.length === 0) { 111 | throw new Error( 112 | `No gRPC call with net peer name (host name) ${netPeerName} ${this.serviceErrorBySpanKind()}`, 113 | ); 114 | } 115 | 116 | return new GrpcRequest(filteredSpans, this.extra); 117 | } 118 | 119 | withHostName(hostName: string, options?: CompareOptions) { 120 | return this.withNetPeerName(hostName, options); 121 | } 122 | 123 | private serviceErrorBySpanKind() { 124 | const { SPAN_KIND_CLIENT, SPAN_KIND_SERVER } = 125 | opentelemetry.proto.trace.v1.Span.SpanKind; 126 | const { spanKind, serviceName } = this.extra; 127 | 128 | switch (spanKind) { 129 | case SPAN_KIND_CLIENT: 130 | return `was sent by ${serviceName}`; 131 | case SPAN_KIND_SERVER: 132 | return `was received by ${serviceName}`; 133 | default: 134 | return `was found for ${serviceName}`; 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/resources/http-request.test.ts: -------------------------------------------------------------------------------- 1 | import { jest, describe, it } from '@jest/globals'; 2 | import { expectTrace } from '../..'; 3 | import { TraceLoop } from '../trace-loop'; 4 | 5 | jest.setTimeout(30000); 6 | 7 | describe('resource http request matchers', () => { 8 | describe('when orders-service makes an http call to emails-service', () => { 9 | let traceloop: TraceLoop; 10 | beforeAll(async () => { 11 | traceloop = new TraceLoop(); 12 | 13 | await traceloop.axiosInstance.post('http://localhost:3000/orders/create'); 14 | await traceloop.fetchTraces(); 15 | }); 16 | 17 | it('should contain outbound http call from orders-service with all parameters', async () => { 18 | expectTrace(traceloop.serviceByName('orders-service')) 19 | .toSendHttpRequest() 20 | .withMethod('POST') 21 | .withUrl('/emails/send', { compareType: 'contains' }) 22 | .withRequestHeader('content-type', 'application/json') 23 | .withRequestBody({ 24 | email: 'test', 25 | nestedObject: { test: 'test' }, 26 | }) 27 | .withRequestBody( 28 | { nestedObject: { test: 'test' } }, 29 | { compareType: 'contains' }, 30 | ) 31 | .withStatusCode(200); 32 | }); 33 | 34 | it('should contain inbound http call to emails-service', async () => { 35 | expectTrace(traceloop.serviceByName('emails-service')) 36 | .toReceiveHttpRequest() 37 | .withMethod('POST') 38 | .withUrl('/emails/send', { compareType: 'contains' }) 39 | .withStatusCode(200); 40 | }); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/resources/http-request.ts: -------------------------------------------------------------------------------- 1 | import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 2 | import { opentelemetry } from '@traceloop/otel-proto'; 3 | import { 4 | CompareOptions, 5 | filterByAttributeIntValue, 6 | filterByAttributeJSON, 7 | filterByAttributeStringValue, 8 | stringCompare, 9 | } from '../matchers/utils'; 10 | 11 | export class HttpRequest { 12 | constructor( 13 | readonly spans: opentelemetry.proto.trace.v1.ISpan[], 14 | readonly extra: { 15 | serviceName: string; 16 | spanKind: opentelemetry.proto.trace.v1.Span.SpanKind; 17 | }, 18 | ) {} 19 | 20 | withRequestBody(body: Record, options?: CompareOptions) { 21 | const filteredSpans = filterByAttributeJSON( 22 | this.spans, 23 | 'http.request.body', 24 | body, 25 | options, 26 | ); 27 | 28 | if (filteredSpans.length === 0) { 29 | throw new Error( 30 | `No HTTP call with request body ${body} ${this.serviceErrorBySpanKind()}`, 31 | ); 32 | } 33 | 34 | return new HttpRequest(filteredSpans, this.extra); 35 | } 36 | 37 | withResponseBody(body: Record, options?: CompareOptions) { 38 | const filteredSpans = filterByAttributeJSON( 39 | this.spans, 40 | 'http.response.body', 41 | body, 42 | options, 43 | ); 44 | 45 | if (filteredSpans.length === 0) { 46 | throw new Error( 47 | `No HTTP call with response body ${body} ${this.serviceErrorBySpanKind()}`, 48 | ); 49 | } 50 | 51 | return new HttpRequest(filteredSpans, this.extra); 52 | } 53 | 54 | withRequestHeader(key: string, value: string, options?: CompareOptions) { 55 | const filteredSpansBySingle = filterByAttributeStringValue( 56 | this.spans, 57 | `http.request.header.${key}`, 58 | value, 59 | options, 60 | ); 61 | 62 | const headerObjectSpans = this.spans.filter((span) => { 63 | const attr = span.attributes?.find( 64 | (attribute) => attribute.key === 'http.request.headers', 65 | ); 66 | try { 67 | const headerObject = JSON.parse(attr?.value?.stringValue ?? ''); 68 | return stringCompare(headerObject[key], value); 69 | } catch (e) { 70 | return false; 71 | } 72 | }); 73 | 74 | const filteredSpans = [...filteredSpansBySingle, ...headerObjectSpans]; 75 | 76 | if (filteredSpans.length === 0) { 77 | throw new Error( 78 | `No HTTP call with request header ${key} assigned with ${value} ${this.serviceErrorBySpanKind()}`, 79 | ); 80 | } 81 | 82 | return new HttpRequest(filteredSpans, this.extra); 83 | } 84 | 85 | withResponseHeader(key: string, value: string, options?: CompareOptions) { 86 | const filteredSpansBySingle = filterByAttributeStringValue( 87 | this.spans, 88 | `http.response.header.${key}`, 89 | value, 90 | options, 91 | ); 92 | 93 | const headerObjectSpans = this.spans.filter((span) => { 94 | const attr = span.attributes?.find( 95 | (attribute) => attribute.key === 'http.response.headers', 96 | ); 97 | try { 98 | const headerObject = JSON.parse(attr?.value?.stringValue ?? ''); 99 | return stringCompare(headerObject[key], value); 100 | } catch (e) { 101 | return false; 102 | } 103 | }); 104 | 105 | const filteredSpans = [...filteredSpansBySingle, ...headerObjectSpans]; 106 | 107 | if (filteredSpans.length === 0) { 108 | throw new Error( 109 | `No HTTP call with response header ${key} assigned with ${value} ${this.serviceErrorBySpanKind()}`, 110 | ); 111 | } 112 | 113 | return new HttpRequest(filteredSpans, this.extra); 114 | } 115 | 116 | withMethod(method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH') { 117 | const filteredSpans = filterByAttributeStringValue( 118 | this.spans, 119 | SemanticAttributes.HTTP_METHOD, 120 | method, 121 | ); 122 | 123 | if (filteredSpans.length === 0) { 124 | throw new Error( 125 | `No HTTP call of method ${method} ${this.serviceErrorBySpanKind()}`, 126 | ); 127 | } 128 | 129 | return new HttpRequest(filteredSpans, this.extra); 130 | } 131 | 132 | withStatusCode(code: number) { 133 | const filteredSpans = filterByAttributeIntValue( 134 | this.spans, 135 | SemanticAttributes.HTTP_STATUS_CODE, 136 | code, 137 | ); 138 | 139 | if (filteredSpans.length === 0) { 140 | throw new Error( 141 | `No HTTP call with status code ${code} ${this.serviceErrorBySpanKind()}`, 142 | ); 143 | } 144 | 145 | return new HttpRequest(filteredSpans, this.extra); 146 | } 147 | 148 | withUrl(url: string, options?: CompareOptions) { 149 | const filteredSpans = filterByAttributeStringValue( 150 | this.spans, 151 | SemanticAttributes.HTTP_URL, 152 | url, 153 | options, 154 | ); 155 | 156 | if (filteredSpans.length === 0) { 157 | throw new Error( 158 | `No HTTP call with url ${url} ${this.serviceErrorBySpanKind()}`, 159 | ); 160 | } 161 | 162 | return new HttpRequest(filteredSpans, this.extra); 163 | } 164 | 165 | private serviceErrorBySpanKind() { 166 | switch (this.extra.spanKind) { 167 | case opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_CLIENT: 168 | return `was sent by ${this.extra.serviceName}`; 169 | case opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_SERVER: 170 | return `was received by ${this.extra.serviceName}`; 171 | default: 172 | return `was found for ${this.extra.serviceName}`; 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/resources/index.ts: -------------------------------------------------------------------------------- 1 | export * from './http-request'; 2 | export * from './service'; 3 | export * from './postgresql-query'; 4 | export * from './grpc-request'; 5 | export * from './redis-command'; 6 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/resources/postgresql-query.ts: -------------------------------------------------------------------------------- 1 | import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 2 | import { opentelemetry } from '@traceloop/otel-proto'; 3 | import { 4 | CompareOptions, 5 | extractAttributeStringValues, 6 | filterByAttributeStringValue, 7 | } from '../matchers/utils'; 8 | 9 | const tablesRegex = /(from|join|into|update|alter)\s+(?\S+)/gi; 10 | 11 | export class PostgreSQLQuery { 12 | constructor( 13 | readonly spans: opentelemetry.proto.trace.v1.ISpan[], 14 | private readonly serviceName: string, 15 | private readonly times = 1, 16 | ) {} 17 | 18 | withDatabaseName(name: string | RegExp, options?: CompareOptions) { 19 | const filteredSpans = filterByAttributeStringValue( 20 | this.spans, 21 | SemanticAttributes.DB_NAME, 22 | name, 23 | options, 24 | ); 25 | 26 | if (filteredSpans.length < this.times) { 27 | throw new Error(`Expected ${this.times} queries by ${ 28 | this.serviceName 29 | } to postgresql with database name ${name}, but found ${ 30 | filteredSpans.length 31 | }.\n 32 | Found db names:\n ${extractAttributeStringValues( 33 | this.spans, 34 | SemanticAttributes.DB_NAME, 35 | )}`); 36 | } 37 | 38 | return new PostgreSQLQuery(filteredSpans, this.serviceName, this.times); 39 | } 40 | 41 | withStatement(statement: string | RegExp, options?: CompareOptions) { 42 | const filteredSpans = filterByAttributeStringValue( 43 | this.spans, 44 | SemanticAttributes.DB_STATEMENT, 45 | statement, 46 | options, 47 | ); 48 | 49 | if (filteredSpans.length < this.times) { 50 | throw new Error(`Expected ${this.times} queries by ${ 51 | this.serviceName 52 | } to postgresql with statement ${statement}, but found ${ 53 | filteredSpans.length 54 | }.\n 55 | Found statements:\n${printStatements(this.spans)}`); 56 | } 57 | 58 | return new PostgreSQLQuery(filteredSpans, this.serviceName, this.times); 59 | } 60 | 61 | withOperations(...operations: string[]) { 62 | const filteredSpans = this.spans.filter((span) => { 63 | const statement = span.attributes?.find( 64 | (attribute: opentelemetry.proto.common.v1.IKeyValue) => 65 | attribute.key === SemanticAttributes.DB_STATEMENT, 66 | )?.value?.stringValue; 67 | 68 | if (!statement) { 69 | return false; 70 | } 71 | 72 | const lowerCaseStatement = statement.toLowerCase(); 73 | 74 | return operations.every((operation) => 75 | lowerCaseStatement.includes(operation.toLowerCase()), 76 | ); 77 | }); 78 | 79 | if (filteredSpans.length < this.times) { 80 | throw new Error( 81 | `Expected ${this.times} queries by ${this.serviceName} to postgresql with operations ${operations}, but found ${filteredSpans.length}.\n` + 82 | `Found statements:\n${printStatements(this.spans)}`, 83 | ); 84 | } 85 | 86 | return new PostgreSQLQuery(filteredSpans, this.serviceName, this.times); 87 | } 88 | 89 | withTables(...tables: string[]) { 90 | const filteredSpans = this.spans.filter((span) => { 91 | const statement = span.attributes?.find( 92 | (attribute: opentelemetry.proto.common.v1.IKeyValue) => 93 | attribute.key === SemanticAttributes.DB_STATEMENT, 94 | )?.value?.stringValue; 95 | 96 | if (!statement) { 97 | return false; 98 | } 99 | 100 | const matches = statement.match(tablesRegex); 101 | const cleaned = matches?.map((elem: string) => { 102 | const [_, second] = elem.split(' '); 103 | return second 104 | .replaceAll('"', '') 105 | .replaceAll('(', '') 106 | .replaceAll(')', '') 107 | .replaceAll('\n', '') 108 | .toLocaleLowerCase(); 109 | }); 110 | 111 | return tables.every((table) => 112 | cleaned?.includes(table.toLocaleLowerCase()), 113 | ); 114 | }); 115 | 116 | if (filteredSpans.length < this.times) { 117 | throw new Error( 118 | `Expected ${this.times} queries by ${this.serviceName} to postgresql with tables ${tables}, but found ${filteredSpans.length}.\n` + 119 | `Found statements:\n${printStatements(this.spans)}`, 120 | ); 121 | } 122 | 123 | return new PostgreSQLQuery(filteredSpans, this.serviceName, this.times); 124 | } 125 | } 126 | 127 | const printStatements = (spans: opentelemetry.proto.trace.v1.ISpan[]) => { 128 | const MAX_LEN = 100; 129 | return extractAttributeStringValues(spans, SemanticAttributes.DB_STATEMENT) 130 | .map((statement) => { 131 | if (statement.length > MAX_LEN) { 132 | return `${statement.slice(0, MAX_LEN)}...`; 133 | } 134 | return statement; 135 | }) 136 | .join('\n'); 137 | }; 138 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/resources/redis-command.ts: -------------------------------------------------------------------------------- 1 | import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 2 | import { opentelemetry } from '@traceloop/otel-proto'; 3 | import { 4 | CompareOptions, 5 | filterByAttributeStringValue, 6 | } from '../matchers/utils'; 7 | 8 | export class RedisCommand { 9 | constructor( 10 | readonly spans: opentelemetry.proto.trace.v1.ISpan[], 11 | private readonly serviceName: string, 12 | private readonly times = 1, 13 | ) {} 14 | 15 | withDatabaseName(name: string | RegExp, options: CompareOptions) { 16 | const filteredSpans = filterByAttributeStringValue( 17 | this.spans, 18 | SemanticAttributes.DB_NAME, 19 | name, 20 | options, 21 | ); 22 | 23 | if (filteredSpans.length < this.times) { 24 | throw new Error( 25 | `Expected ${this.times} queries by ${this.serviceName} to redis with database name ${name}, but found ${filteredSpans.length}.`, 26 | ); 27 | } 28 | 29 | return new RedisCommand(filteredSpans, this.serviceName); 30 | } 31 | 32 | withStatement(statement: string | RegExp, options: CompareOptions) { 33 | const filteredSpans = filterByAttributeStringValue( 34 | this.spans, 35 | SemanticAttributes.DB_STATEMENT, 36 | statement, 37 | options, 38 | ); 39 | 40 | if (filteredSpans.length < this.times) { 41 | throw new Error( 42 | `Expected ${this.times} queries by ${this.serviceName} to redis with statement ${statement}, but found ${filteredSpans.length}.`, 43 | ); 44 | } 45 | 46 | return new RedisCommand(filteredSpans, this.serviceName); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/resources/service.ts: -------------------------------------------------------------------------------- 1 | import { opentelemetry } from '@traceloop/otel-proto'; 2 | 3 | export class Service { 4 | constructor( 5 | public readonly name: string, 6 | public readonly spans: opentelemetry.proto.trace.v1.ISpan[], 7 | ) {} 8 | } 9 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/trace-loop/fetch-traces.ts: -------------------------------------------------------------------------------- 1 | import { opentelemetry } from '@traceloop/otel-proto'; 2 | import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 3 | import { setTimeout } from 'timers/promises'; 4 | import { httpGetBinary } from '../utils'; 5 | 6 | export interface FetchTracesConfig { 7 | maxPollTime: number; 8 | pollInterval: number; 9 | awaitAllSpansInTraceTimeout: number; 10 | url: string; 11 | apiKey: string; 12 | } 13 | 14 | export const fetchTracesConfigBase: FetchTracesConfig = { 15 | maxPollTime: 9000, 16 | pollInterval: 1000, 17 | awaitAllSpansInTraceTimeout: 2000, 18 | url: process.env.TRACELOOP_URL || 'https://api.traceloop.dev/v1/traces', 19 | apiKey: process.env.TRACELOOP_API_KEY || 'none', 20 | }; 21 | 22 | /** 23 | * Searches in the traces for a trace with the given traceLoopId contained in the attribute http.user_agent 24 | * 25 | * @param traces all traces from the otel receiver 26 | * @param traceLoopId traceLoopId to search for 27 | * @returns traceId of the span with the given traceLoopId or undefined if no match was found 28 | */ 29 | export const findTraceLoopIdMatch = ( 30 | traces: opentelemetry.proto.trace.v1.TracesData, 31 | traceLoopId: string, 32 | ): string | undefined => { 33 | for (const resourceSpan of traces.resourceSpans) { 34 | for (const scopeSpan of resourceSpan.scopeSpans || []) { 35 | for (const span of scopeSpan.spans || []) { 36 | if (span.attributes) { 37 | for (const attribute of span.attributes) { 38 | if (attribute.key === SemanticAttributes.HTTP_USER_AGENT) { 39 | const matches = 40 | attribute.value?.stringValue?.match(/traceloop_id=(.*)/); 41 | if (matches?.length > 1) { 42 | if (matches[1] === traceLoopId) { 43 | return span.traceId 44 | ? Buffer.from(span.traceId).toString('hex') 45 | : undefined; 46 | } 47 | } 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | }; 55 | 56 | export const pollForTraceLoopIdMatch = async ( 57 | config: FetchTracesConfig, 58 | traceLoopId: string, 59 | ): Promise => { 60 | let numOfPolls = Math.floor(config.maxPollTime / config.pollInterval); 61 | let foundMatch = false; 62 | 63 | while (!foundMatch && numOfPolls-- > 0) { 64 | await setTimeout(config.pollInterval); 65 | try { 66 | const response = await httpGetBinary(config, traceLoopId); 67 | const traces = opentelemetry.proto.trace.v1.TracesData.decode(response); 68 | 69 | const traceId = findTraceLoopIdMatch(traces, traceLoopId); 70 | if (traceId) { 71 | foundMatch = true; 72 | return traceId; 73 | } 74 | } catch (e) { 75 | // retry on 400, else throw 76 | if ((e as Error)?.message !== '400') { 77 | throw e; 78 | } 79 | } 80 | } 81 | 82 | return undefined; 83 | }; 84 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/trace-loop/filter-service-spans.ts: -------------------------------------------------------------------------------- 1 | import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; 2 | import { opentelemetry } from '@traceloop/otel-proto'; 3 | import { stringCompare, CompareOptions } from '../matchers/utils'; 4 | 5 | const flatten = ( 6 | serviceResourceSpans: 7 | | opentelemetry.proto.trace.v1.IResourceSpans[] 8 | | undefined, 9 | ) => 10 | ( 11 | serviceResourceSpans?.flatMap((srs) => { 12 | return srs.scopeSpans?.flatMap((ss) => ss.spans || []); 13 | }) || [] 14 | ).filter((s) => !!s) as opentelemetry.proto.trace.v1.ISpan[]; 15 | 16 | const filterByTraceId = ( 17 | spans: opentelemetry.proto.trace.v1.ISpan[] | undefined = [], 18 | traceId: string | undefined, 19 | ) => { 20 | if (!traceId) { 21 | return spans; 22 | } 23 | 24 | return ( 25 | spans.filter( 26 | (span) => 27 | span.traceId && Buffer.from(span.traceId).toString('hex') === traceId, 28 | ) || [] 29 | ); 30 | }; 31 | 32 | export const byCustomAttribute = ( 33 | attName: string, 34 | attValue: string | RegExp, 35 | traceData: opentelemetry.proto.trace.v1.TracesData | undefined, 36 | traceId: string | undefined, 37 | options?: CompareOptions, 38 | ) => { 39 | const serviceResourceSpans = traceData?.resourceSpans?.filter((rs) => 40 | rs.resource?.attributes?.find( 41 | (a) => 42 | a.key === attName && 43 | stringCompare(a.value?.stringValue, attValue, options), 44 | ), 45 | ); 46 | 47 | return filterByTraceId(flatten(serviceResourceSpans), traceId); 48 | }; 49 | 50 | export const byServiceName = ( 51 | name: string, 52 | traceData: opentelemetry.proto.trace.v1.TracesData | undefined, 53 | traceId: string | undefined, 54 | options?: CompareOptions, 55 | ) => 56 | byCustomAttribute( 57 | SemanticResourceAttributes.SERVICE_NAME, 58 | name, 59 | traceData, 60 | traceId, 61 | options, 62 | ); 63 | 64 | export const byK8sPodName = ( 65 | name: string, 66 | traceData: opentelemetry.proto.trace.v1.TracesData | undefined, 67 | traceId: string | undefined, 68 | options?: CompareOptions, 69 | ) => 70 | byCustomAttribute( 71 | SemanticResourceAttributes.K8S_POD_NAME, 72 | name, 73 | traceData, 74 | traceId, 75 | options, 76 | ); 77 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/trace-loop/index.ts: -------------------------------------------------------------------------------- 1 | import { randomUUID } from 'crypto'; 2 | import axios from 'axios'; 3 | import { opentelemetry } from '@traceloop/otel-proto'; 4 | import { httpGetBinary } from '../utils'; 5 | import { setTimeout } from 'timers/promises'; 6 | import { 7 | fetchTracesConfigBase, 8 | FetchTracesConfig, 9 | pollForTraceLoopIdMatch, 10 | } from './fetch-traces'; 11 | import { Service } from '../resources/service'; 12 | import { CompareOptions } from '../matchers/utils/compare-types'; 13 | import { 14 | byK8sPodName, 15 | byServiceName, 16 | byCustomAttribute, 17 | } from './filter-service-spans'; 18 | 19 | export class TraceLoop { 20 | private readonly _traceLoopId: string; 21 | private _fetchedTrace = false; 22 | private _traceId: string | undefined; 23 | private _traceData: opentelemetry.proto.trace.v1.TracesData | undefined; 24 | 25 | constructor() { 26 | this._traceLoopId = randomUUID(); 27 | } 28 | 29 | get traceLoopId() { 30 | return this._traceLoopId; 31 | } 32 | 33 | get axiosInstance() { 34 | return axios.create({ 35 | headers: { 'User-Agent': `traceloop_id=${this._traceLoopId}` }, 36 | }); 37 | } 38 | 39 | /** 40 | * Fetches all traces for the current trace loop id from the otel receiver 41 | * @param config 42 | */ 43 | public async fetchTraces( 44 | inputConfig: Partial = {}, 45 | ): Promise { 46 | const config = { ...fetchTracesConfigBase, ...inputConfig }; 47 | if (this._fetchedTrace) { 48 | return; 49 | } 50 | 51 | this._traceId = await pollForTraceLoopIdMatch(config, this._traceLoopId); 52 | 53 | // allow time for all spans for the current trace to be received 54 | await setTimeout(config.awaitAllSpansInTraceTimeout); 55 | 56 | const response = await httpGetBinary(config, this._traceLoopId); 57 | this._traceData = opentelemetry.proto.trace.v1.TracesData.decode(response); 58 | this._fetchedTrace = true; 59 | } 60 | 61 | public serviceByName(name: string, options?: CompareOptions) { 62 | return new Service( 63 | name, 64 | byServiceName(name, this._traceData, this._traceId, options), 65 | ); 66 | } 67 | 68 | public serviceByK8sPodName(name: string, options?: CompareOptions) { 69 | return new Service( 70 | name, 71 | byK8sPodName(name, this._traceData, this._traceId, options), 72 | ); 73 | } 74 | 75 | public serviceByCustomAttribute( 76 | key: string, 77 | value: string | RegExp, 78 | options?: CompareOptions, 79 | ) { 80 | return new Service( 81 | value as string, 82 | byCustomAttribute(key, value, this._traceData, this._traceId, options), 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /packages/expect-opentelemetry/src/utils.ts: -------------------------------------------------------------------------------- 1 | import http from 'http'; 2 | import https from 'https'; 3 | import { opentelemetry } from '@traceloop/otel-proto'; 4 | import { FetchTracesConfig } from './trace-loop/fetch-traces'; 5 | 6 | export const getInstanceType = (instance: any) => { 7 | if ( 8 | instance?.constructor?.name && 9 | ['Span', 'Service'].includes(instance.constructor.name) 10 | ) { 11 | return instance.constructor.name; 12 | } 13 | 14 | return null; 15 | }; 16 | 17 | export const enhanceError = (error: any, message: string) => { 18 | error.message = `${message}\n${error.message}`; 19 | return error; 20 | }; 21 | 22 | export function parseServerResponse( 23 | data: any, 24 | ): opentelemetry.proto.trace.v1.TracesData { 25 | const tracesBinary = Buffer.from(data.traces, 'base64'); 26 | 27 | return opentelemetry.proto.trace.v1.TracesData.decode(tracesBinary); 28 | } 29 | 30 | export const generateStubData = () => { 31 | const tracesData = opentelemetry.proto.trace.v1.TracesData.create(); 32 | 33 | tracesData.resourceSpans.push( 34 | opentelemetry.proto.trace.v1.ResourceSpans.create({ 35 | resource: opentelemetry.proto.resource.v1.Resource.create({ 36 | attributes: [ 37 | opentelemetry.proto.common.v1.KeyValue.create({ 38 | key: 'service.name', 39 | value: opentelemetry.proto.common.v1.AnyValue.create({ 40 | stringValue: 'orders-service', 41 | }), 42 | }), 43 | ], 44 | }), 45 | scopeSpans: [ 46 | opentelemetry.proto.trace.v1.ScopeSpans.create({ 47 | spans: [ 48 | opentelemetry.proto.trace.v1.Span.create({ 49 | traceId: Uint8Array.from(Buffer.from('AAAAAAAAAAAAAAAA', 'hex')), 50 | spanId: Uint8Array.from(Buffer.from('BBBBBBBBBBBBBBBB', 'hex')), 51 | name: 'orders-service', 52 | kind: opentelemetry.proto.trace.v1.Span.SpanKind.SPAN_KIND_CLIENT, 53 | startTimeUnixNano: 1630000000000000000, 54 | endTimeUnixNano: 1630000000000000000, 55 | attributes: [ 56 | opentelemetry.proto.common.v1.KeyValue.create({ 57 | key: 'http.method', 58 | value: opentelemetry.proto.common.v1.AnyValue.create({ 59 | stringValue: 'GET', 60 | }), 61 | }), 62 | opentelemetry.proto.common.v1.KeyValue.create({ 63 | key: 'http.url', 64 | value: opentelemetry.proto.common.v1.AnyValue.create({ 65 | stringValue: 'http://localhost:3000/orders', 66 | }), 67 | }), 68 | ], 69 | }), 70 | ], 71 | }), 72 | ], 73 | }), 74 | ); 75 | 76 | return opentelemetry.proto.trace.v1.TracesData.encode(tracesData).finish(); 77 | }; 78 | 79 | /** 80 | * Promise wrapper for http.get 81 | * @see https://github.com/protobufjs/protobuf.js/wiki/How-to-read-binary-data-in-the-browser-or-under-node.js%3F 82 | * 83 | * @param url - url to make get request to (server that responds with Buffer) 84 | * @returns Buffer result 85 | */ 86 | export function httpGetBinary( 87 | config: FetchTracesConfig, 88 | traceloopId: string, 89 | ): Promise { 90 | const url = `${config.url}/${traceloopId}`; 91 | return new Promise((resolve, reject) => { 92 | const method = config.url.includes('https') ? https : http; 93 | method.get(url, { headers: { Authorization: config.apiKey } }, (res) => { 94 | const { statusCode } = res; 95 | 96 | if (!statusCode || statusCode < 200 || statusCode >= 300) { 97 | return reject(new Error(`${res.statusCode}`)); 98 | } 99 | 100 | const data: Uint8Array[] = []; 101 | res.on('data', function (chunk) { 102 | data.push(chunk as never); 103 | }); 104 | res.on('end', function () { 105 | const result = Buffer.concat(data); 106 | resolve(result); 107 | }); 108 | res.on('error', function () { 109 | reject('Error while fetching data'); 110 | }); 111 | }); 112 | }); 113 | } 114 | -------------------------------------------------------------------------------- /packages/instrument-opentelemetry/.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /packages/instrument-opentelemetry/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [0.8.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.7.0...v0.8.0) (2023-06-20) 7 | 8 | **Note:** Version bump only for package @traceloop/instrument-opentelemetry 9 | 10 | # [0.7.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.6.0...v0.7.0) (2023-06-13) 11 | 12 | **Note:** Version bump only for package @traceloop/instrument-opentelemetry 13 | 14 | # [0.6.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.5.0...v0.6.0) (2023-06-04) 15 | 16 | **Note:** Version bump only for package @traceloop/instrument-opentelemetry 17 | 18 | # [0.5.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.4.1...v0.5.0) (2023-03-17) 19 | 20 | ### Features 21 | 22 | - go server backend ([#42](https://github.com/traceloop/jest-opentelemetry/issues/42)) ([a543845](https://github.com/traceloop/jest-opentelemetry/commit/a543845445617bd321c7cee793e23caf2c651844)) 23 | 24 | ## [0.4.1](https://github.com/traceloop/jest-opentelemetry/compare/v0.4.0...v0.4.1) (2023-02-23) 25 | 26 | ### Bug Fixes 27 | 28 | - publish on public packages ([7ecb00a](https://github.com/traceloop/jest-opentelemetry/commit/7ecb00aff9b376195c3b04a2f65dcd26321ba186)) 29 | - tracing enabled only if OTEL_EXPORTER_OTLP_ENDPOINT is specified ([80f74ce](https://github.com/traceloop/jest-opentelemetry/commit/80f74ce6c06ea1b6740faa5cf8499ea70d284cdf)) 30 | 31 | # 0.4.0 (2023-02-22) 32 | 33 | ### Features 34 | 35 | - **database-matchers:** support postgres matchers ([#24](https://github.com/traceloop/jest-opentelemetry/issues/24)) ([8b86a4b](https://github.com/traceloop/jest-opentelemetry/commit/8b86a4b7c926498c00a3eaa3da326d45eeda8d77)) 36 | - k8s init-container ([c05bda4](https://github.com/traceloop/jest-opentelemetry/commit/c05bda437b9b11e5097e482f6a7885e58789cc5b)) 37 | - separate otel instrumentation into a new package ([dfe7586](https://github.com/traceloop/jest-opentelemetry/commit/dfe758613c5e17c4e92144023c82d91526aca786)) 38 | -------------------------------------------------------------------------------- /packages/instrument-opentelemetry/Dockerfile: -------------------------------------------------------------------------------- 1 | ## Following instructions found in: 2 | ## https://github.com/open-telemetry/opentelemetry-operator/blob/main/autoinstrumentation/nodejs/Dockerfile 3 | 4 | FROM node:16 AS build 5 | 6 | WORKDIR /operator-build 7 | 8 | COPY package*.json ./ 9 | COPY packages/instrument-opentelemetry/package*.json ./packages/instrument-opentelemetry/ 10 | 11 | RUN npm install 12 | 13 | COPY lerna.json ./ 14 | COPY tsconfig.json ./ 15 | COPY packages/instrument-opentelemetry/. ./packages/instrument-opentelemetry/ 16 | 17 | RUN npm run build 18 | 19 | FROM busybox AS prod 20 | 21 | COPY --from=build /operator-build/node_modules /autoinstrumentation/node_modules 22 | COPY --from=build /operator-build/packages/instrument-opentelemetry/dist/tracing.js /autoinstrumentation/autoinstrumentation.js 23 | 24 | RUN chmod -R go+r /autoinstrumentation -------------------------------------------------------------------------------- /packages/instrument-opentelemetry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@traceloop/instrument-opentelemetry", 3 | "version": "0.8.0", 4 | "main": "./dist/tracing.js", 5 | "engines": { 6 | "node": ">=14.0.0" 7 | }, 8 | "publishConfig": { 9 | "access": "public" 10 | }, 11 | "scripts": { 12 | "prebuild": "rm -rf dist", 13 | "build": "rollup -c" 14 | }, 15 | "devDependencies": { 16 | "@types/express": "^4.17.17", 17 | "rollup": "^3.20.0", 18 | "rollup-plugin-swc3": "^0.8.0" 19 | }, 20 | "dependencies": { 21 | "@opentelemetry/api": "^1.4.1", 22 | "@opentelemetry/auto-instrumentations-node": "^0.36.4", 23 | "@opentelemetry/exporter-trace-otlp-grpc": "0.35.1", 24 | "@opentelemetry/exporter-trace-otlp-proto": "^0.36.1", 25 | "@opentelemetry/instrumentation-http": "^0.35.1", 26 | "@opentelemetry/resource-detector-container": "^0.2.2", 27 | "@opentelemetry/sdk-node": "^0.35.1", 28 | "@opentelemetry/semantic-conventions": "^1.10.1", 29 | "@opentelemetry/tracing": "^0.24.0", 30 | "opentelemetry-instrumentation-express": "^0.34.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/instrument-opentelemetry/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { swc, defineRollupSwcOption } from 'rollup-plugin-swc3'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | 4 | const bundle = (config) => ({ 5 | external: (id) => { 6 | return !/^[./]/.test(id); 7 | }, 8 | ...config, 9 | }); 10 | 11 | const swcPlugin = swc( 12 | defineRollupSwcOption({ 13 | jsc: { target: 'es2021' }, 14 | }), 15 | ); 16 | 17 | export default [ 18 | bundle({ 19 | input: ['src/tracing.ts'], 20 | output: { 21 | file: 'dist/tracing.js', 22 | format: 'cjs', 23 | }, 24 | plugins: [swcPlugin, typescript({ tsconfig: '../../tsconfig.json' })], 25 | }), 26 | ]; 27 | -------------------------------------------------------------------------------- /packages/instrument-opentelemetry/src/otel-custom/constants.js: -------------------------------------------------------------------------------- 1 | export const HttpExtendedAttribute = { 2 | HTTP_REQUEST_HEADERS: 'http.request.headers', 3 | HTTP_REQUEST_BODY: 'http.request.body', 4 | HTTP_RESPONSE_HEADERS: 'http.response.headers', 5 | HTTP_RESPONSE_BODY: 'http.response.body', 6 | HTTP_PATH: 'http.path', 7 | }; 8 | -------------------------------------------------------------------------------- /packages/instrument-opentelemetry/src/otel-custom/express.js: -------------------------------------------------------------------------------- 1 | import { HttpExtendedAttribute } from './constants'; 2 | import { shouldCaptureBodyByMimeType } from './mime-type'; 3 | import { StreamChunks } from './stream-chunks'; 4 | 5 | const requestHook = (span, req, res) => { 6 | span.setAttributes({ 7 | [HttpExtendedAttribute.HTTP_PATH]: req.path, 8 | [HttpExtendedAttribute.HTTP_REQUEST_HEADERS]: JSON.stringify(req.headers), 9 | }); 10 | 11 | const requestMimeType = req.get('content-type'); 12 | const captureRequestBody = shouldCaptureBodyByMimeType(requestMimeType); 13 | const requestStreamChunks = new StreamChunks(); 14 | 15 | if (captureRequestBody) { 16 | req.on('data', (chunk) => requestStreamChunks.addChunk(chunk)); 17 | } 18 | 19 | const responseStreamChunks = new StreamChunks(); 20 | 21 | const originalResWrite = res.write; 22 | 23 | res.write = function (chunk) { 24 | responseStreamChunks.addChunk(chunk); 25 | originalResWrite.apply(res, arguments); 26 | }; 27 | 28 | const oldResEnd = res.end; 29 | res.end = function (chunk) { 30 | oldResEnd.apply(res, arguments); 31 | 32 | const responseMimeType = res.get('content-type'); 33 | const captureResponseBody = shouldCaptureBodyByMimeType(responseMimeType); 34 | if (captureResponseBody) responseStreamChunks.addChunk(chunk); 35 | 36 | span.setAttributes({ 37 | [HttpExtendedAttribute.HTTP_REQUEST_BODY]: captureRequestBody 38 | ? requestStreamChunks.getBody() 39 | : `Request body not collected due to unsupported mime type: ${requestMimeType}`, 40 | [HttpExtendedAttribute.HTTP_RESPONSE_BODY]: captureResponseBody 41 | ? responseStreamChunks.getBody() 42 | : `Response body not collected due to unsupported mime type: ${responseMimeType}`, 43 | }); 44 | 45 | span.setAttributes({ 46 | [HttpExtendedAttribute.HTTP_RESPONSE_HEADERS]: JSON.stringify( 47 | res.getHeaders(), 48 | ), 49 | }); 50 | }; 51 | }; 52 | 53 | export const expressInstrumentationConfig = { 54 | requestHook, 55 | }; 56 | -------------------------------------------------------------------------------- /packages/instrument-opentelemetry/src/otel-custom/http.js: -------------------------------------------------------------------------------- 1 | import { IncomingMessage, ClientRequest } from 'http'; 2 | import { HttpExtendedAttribute } from './constants'; 3 | import { shouldCaptureBodyByMimeType } from './mime-type'; 4 | import { StreamChunks } from './stream-chunks'; 5 | 6 | const streamChunksKey = Symbol( 7 | 'opentelemetry.instrumentation.http.StreamChunks', 8 | ); 9 | 10 | const httpCustomAttributes = (span, request, response) => { 11 | if (request instanceof ClientRequest) { 12 | const reqPath = request.path.split('?')[0]; 13 | span.setAttribute(HttpExtendedAttribute.HTTP_PATH, reqPath); 14 | span.setAttribute( 15 | HttpExtendedAttribute.HTTP_REQUEST_HEADERS, 16 | JSON.stringify(request.getHeaders()), 17 | ); 18 | } 19 | if (response instanceof IncomingMessage) { 20 | span.setAttribute( 21 | HttpExtendedAttribute.HTTP_RESPONSE_HEADERS, 22 | JSON.stringify(response.headers), 23 | ); 24 | } 25 | 26 | const requestBody = request[streamChunksKey]; 27 | if (requestBody) { 28 | span.setAttribute( 29 | HttpExtendedAttribute.HTTP_REQUEST_BODY, 30 | requestBody.getBody(), 31 | ); 32 | } 33 | 34 | const responseBody = response[streamChunksKey]; 35 | if (responseBody) { 36 | span.setAttribute( 37 | HttpExtendedAttribute.HTTP_RESPONSE_BODY, 38 | responseBody.getBody(), 39 | ); 40 | } 41 | }; 42 | 43 | const httpCustomAttributesOnRequest = (span, request) => { 44 | if (request instanceof ClientRequest) { 45 | const requestMimeType = request.getHeader('content-type'); 46 | if (!shouldCaptureBodyByMimeType(requestMimeType)) { 47 | span.setAttribute( 48 | HttpExtendedAttribute.HTTP_REQUEST_BODY, 49 | `Request body not collected due to unsupported mime type: ${requestMimeType}`, 50 | ); 51 | return; 52 | } 53 | 54 | let oldWrite = request.write; 55 | request[streamChunksKey] = new StreamChunks(); 56 | request.write = function (data) { 57 | const expectDevData = request[streamChunksKey]; 58 | expectDevData?.addChunk(data); 59 | return oldWrite.call(request, data); 60 | }; 61 | } 62 | }; 63 | 64 | const httpCustomAttributesOnResponse = (span, response) => { 65 | if (response instanceof IncomingMessage) { 66 | const responseMimeType = response.headers?.['content-type']; 67 | if (!shouldCaptureBodyByMimeType(responseMimeType)) { 68 | span.setAttribute( 69 | HttpExtendedAttribute.HTTP_RESPONSE_BODY, 70 | `Response body not collected due to unsupported mime type: ${responseMimeType}`, 71 | ); 72 | return; 73 | } 74 | 75 | response[streamChunksKey] = new StreamChunks(); 76 | const origPush = response.push; 77 | response.push = function (chunk) { 78 | if (chunk) { 79 | const expectDevData = response[streamChunksKey]; 80 | expectDevData?.addChunk(chunk); 81 | } 82 | return origPush.apply(this, arguments); 83 | }; 84 | } 85 | }; 86 | 87 | export const httpInstrumentationConfig = { 88 | applyCustomAttributesOnSpan: httpCustomAttributes, 89 | requestHook: httpCustomAttributesOnRequest, 90 | responseHook: httpCustomAttributesOnResponse, 91 | headersToSpanAttributes: { 92 | client: { 93 | requestHeaders: ['traceloop_id'], 94 | responseHeaders: ['traceloop_id'], 95 | }, 96 | server: { 97 | requestHeaders: ['traceloop_id'], 98 | responseHeaders: ['traceloop_id'], 99 | }, 100 | }, 101 | }; 102 | -------------------------------------------------------------------------------- /packages/instrument-opentelemetry/src/otel-custom/mime-type.js: -------------------------------------------------------------------------------- 1 | const allowedMimeTypePrefix = [ 2 | 'text', 3 | 'multipart/form-data', 4 | 'application/json', 5 | 'application/ld+json', 6 | 'application/rtf', 7 | 'application/x-www-form-urlencoded', 8 | 'application/xml', 9 | 'application/xhtml', 10 | ]; 11 | 12 | export const shouldCaptureBodyByMimeType = (mimeType) => { 13 | try { 14 | return ( 15 | !mimeType || 16 | allowedMimeTypePrefix.some((prefix) => mimeType.startsWith(prefix)) 17 | ); 18 | } catch { 19 | return true; 20 | } 21 | }; 22 | -------------------------------------------------------------------------------- /packages/instrument-opentelemetry/src/otel-custom/stream-chunks.js: -------------------------------------------------------------------------------- 1 | // for body with at most this length, full body will be captured. 2 | // for large body with more than this amount of bytes, we will 3 | // collect at least this amount of bytes, but might truncate after it 4 | export const MIN_COLLECTED_BODY_LENGTH = 524288; 5 | 6 | export class StreamChunks { 7 | chunks; 8 | length; 9 | 10 | constructor() { 11 | this.chunks = []; 12 | this.length = 0; 13 | } 14 | 15 | addChunk(chunk) { 16 | if (this.length >= MIN_COLLECTED_BODY_LENGTH) return; 17 | 18 | const chunkLength = chunk?.length; 19 | if (!chunkLength) return; 20 | 21 | this.chunks.push(chunk); 22 | this.length += chunkLength; 23 | } 24 | 25 | getBody() { 26 | return this.chunks.join(''); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/instrument-opentelemetry/src/tracing.ts: -------------------------------------------------------------------------------- 1 | import opentelemetry from '@opentelemetry/sdk-node'; 2 | import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; 3 | import { Resource } from '@opentelemetry/resources'; 4 | import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; 5 | import { httpInstrumentationConfig } from './otel-custom/http'; 6 | import { expressInstrumentationConfig } from './otel-custom/express'; 7 | import { OTLPTraceExporter as ProtoExporter } from '@opentelemetry/exporter-trace-otlp-proto'; 8 | import { OTLPTraceExporter as GRPCExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; 9 | import { containerDetector } from '@opentelemetry/resource-detector-container'; 10 | 11 | if (process.env.OTEL_EXPORTER_OTLP_ENDPOINT) { 12 | const traceExporter = 13 | process.env.OTEL_EXPORTER_TYPE === 'PROTO' 14 | ? new ProtoExporter({ 15 | url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, 16 | timeoutMillis: 100, 17 | }) 18 | : new GRPCExporter({ 19 | url: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, 20 | timeoutMillis: 100, 21 | }); 22 | 23 | const sdk = new opentelemetry.NodeSDK({ 24 | resource: new Resource( 25 | process.env.SERVICE_NAME 26 | ? { 27 | [SemanticResourceAttributes.SERVICE_NAME]: process.env.SERVICE_NAME, 28 | } 29 | : {}, 30 | ), 31 | traceExporter, 32 | instrumentations: [ 33 | getNodeAutoInstrumentations({ 34 | '@opentelemetry/instrumentation-http': httpInstrumentationConfig, 35 | '@opentelemetry/instrumentation-express': expressInstrumentationConfig, 36 | '@opentelemetry/instrumentation-pg': { 37 | enhancedDatabaseReporting: true, 38 | }, 39 | }), 40 | ], 41 | resourceDetectors: [containerDetector], 42 | }); 43 | 44 | // initialize the SDK and register with the OpenTelemetry API 45 | // this enables the API to record telemetry 46 | sdk.start(); 47 | 48 | // gracefully shut down the SDK on process exit 49 | process.on('SIGTERM', () => { 50 | sdk 51 | .shutdown() 52 | .then(() => console.log('Tracing terminated')) 53 | .catch((error) => console.log('Error terminating tracing', error)) 54 | .finally(() => process.exit(0)); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /packages/jest-environment-otel/.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/dist/**/*.js 3 | !index.js 4 | !setup.js 5 | !teardown.js 6 | -------------------------------------------------------------------------------- /packages/jest-environment-otel/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [0.8.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.7.0...v0.8.0) (2023-06-20) 7 | 8 | **Note:** Version bump only for package @traceloop/jest-environment-otel 9 | 10 | # [0.7.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.6.0...v0.7.0) (2023-06-13) 11 | 12 | **Note:** Version bump only for package @traceloop/jest-environment-otel 13 | 14 | # [0.6.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.5.0...v0.6.0) (2023-06-04) 15 | 16 | ### Bug Fixes 17 | 18 | - work with remote server ([#69](https://github.com/traceloop/jest-opentelemetry/issues/69)) ([d219a40](https://github.com/traceloop/jest-opentelemetry/commit/d219a402ea52b7cc7aa4c8f31a4e9071e016a9cf)) 19 | 20 | # [0.5.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.4.1...v0.5.0) (2023-03-17) 21 | 22 | ### Features 23 | 24 | - go server backend ([#42](https://github.com/traceloop/jest-opentelemetry/issues/42)) ([a543845](https://github.com/traceloop/jest-opentelemetry/commit/a543845445617bd321c7cee793e23caf2c651844)) 25 | 26 | # 0.4.0 (2023-02-22) 27 | 28 | ### Bug Fixes 29 | 30 | - adaptations for minimal working js project ([57c94a4](https://github.com/traceloop/jest-opentelemetry/commit/57c94a448045ac00a1c01788a8439489a2ca6fe2)) 31 | - adaptations for usage with minimal js project ([7a7771e](https://github.com/traceloop/jest-opentelemetry/commit/7a7771e165b8924778e82758504ee38b1cdd93bc)) 32 | - compilation & tests ([4e8698f](https://github.com/traceloop/jest-opentelemetry/commit/4e8698f254bad209becf2bd260679831fb25c0a6)) 33 | - created a separate package for protobuf code ([b445f6f](https://github.com/traceloop/jest-opentelemetry/commit/b445f6fb7a32989b44253d813c4f92ad5dfffd2f)) 34 | - imports from dist ([#16](https://github.com/traceloop/jest-opentelemetry/issues/16)) ([230849d](https://github.com/traceloop/jest-opentelemetry/commit/230849d617887d53b85a012cd1878dbdfca19e7b)) 35 | - **integration:** integration test dsl with test servers ([#4](https://github.com/traceloop/jest-opentelemetry/issues/4)) ([6b4b7f7](https://github.com/traceloop/jest-opentelemetry/commit/6b4b7f7c43f7f450b615bf46635a817df708517e)) 36 | - **lint:** linting issues ([#11](https://github.com/traceloop/jest-opentelemetry/issues/11)) ([c74ca77](https://github.com/traceloop/jest-opentelemetry/commit/c74ca77896c35a8ff72500451d41cc0c53d1c8b1)) 37 | - npm publish files ([02afb0b](https://github.com/traceloop/jest-opentelemetry/commit/02afb0b6f6054d3570d2160d667f05033243401d)) 38 | - otel receiver timeouts ([#17](https://github.com/traceloop/jest-opentelemetry/issues/17)) ([8c99815](https://github.com/traceloop/jest-opentelemetry/commit/8c998151a7a30c1a401aa1403587f35026d4814b)) 39 | - separate expect() function ([e704f43](https://github.com/traceloop/jest-opentelemetry/commit/e704f43b5c5877c254c901825c446eeaea2f1258)) 40 | - timeout on test setup ([2b3fc15](https://github.com/traceloop/jest-opentelemetry/commit/2b3fc15b2cfae8e46471c3d11531bff40bf7c834)) 41 | - typescript adaptations ([ec0ad8e](https://github.com/traceloop/jest-opentelemetry/commit/ec0ad8e40c799f70881ae7ac3b441c7f6eb6c9cd)) 42 | - wrong export for setup/teardown of jest ([3979c3f](https://github.com/traceloop/jest-opentelemetry/commit/3979c3f01a0286175c2f1a52bd117b1fee9b6d76)) 43 | 44 | ### Features 45 | 46 | - basic integration with otel-receiver ([10f4a4e](https://github.com/traceloop/jest-opentelemetry/commit/10f4a4e274079620f8461eb64b09c73e9a609077)) 47 | - TraceLoop ([#19](https://github.com/traceloop/jest-opentelemetry/issues/19)) ([ab17a91](https://github.com/traceloop/jest-opentelemetry/commit/ab17a91601ff8643ed99a6e2adaced491ab0743d)) 48 | 49 | # [0.3.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.7...v0.3.0) (2023-02-21) 50 | 51 | **Note:** Version bump only for package @traceloop/jest-environment-otel 52 | 53 | ## [0.2.7](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.6...v0.2.7) (2023-02-21) 54 | 55 | **Note:** Version bump only for package @traceloop/jest-environment-otel 56 | 57 | ## [0.2.6](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.5...v0.2.6) (2023-02-21) 58 | 59 | ### Bug Fixes 60 | 61 | - created a separate package for protobuf code ([b445f6f](https://github.com/traceloop/jest-opentelemetry/commit/b445f6fb7a32989b44253d813c4f92ad5dfffd2f)) 62 | 63 | ## [0.2.5](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.4...v0.2.5) (2023-02-21) 64 | 65 | **Note:** Version bump only for package @traceloop/jest-environment-otel 66 | 67 | ## [0.2.4](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.3...v0.2.4) (2023-02-21) 68 | 69 | ### Bug Fixes 70 | 71 | - separate expect() function ([e704f43](https://github.com/traceloop/jest-opentelemetry/commit/e704f43b5c5877c254c901825c446eeaea2f1258)) 72 | 73 | ## [0.2.3](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.2...v0.2.3) (2023-02-21) 74 | 75 | ### Bug Fixes 76 | 77 | - wrong export for setup/teardown of jest ([3979c3f](https://github.com/traceloop/jest-opentelemetry/commit/3979c3f01a0286175c2f1a52bd117b1fee9b6d76)) 78 | 79 | ## [0.2.2](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.1...v0.2.2) (2023-02-21) 80 | 81 | ### Bug Fixes 82 | 83 | - adaptations for usage with minimal js project ([7a7771e](https://github.com/traceloop/jest-opentelemetry/commit/7a7771e165b8924778e82758504ee38b1cdd93bc)) 84 | 85 | ## [0.2.1](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.0...v0.2.1) (2023-02-21) 86 | 87 | ### Bug Fixes 88 | 89 | - adaptations for minimal working js project ([57c94a4](https://github.com/traceloop/jest-opentelemetry/commit/57c94a448045ac00a1c01788a8439489a2ca6fe2)) 90 | 91 | # [0.2.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.8...v0.2.0) (2023-02-21) 92 | 93 | ### Features 94 | 95 | - TraceLoop ([#19](https://github.com/traceloop/jest-opentelemetry/issues/19)) ([ab17a91](https://github.com/traceloop/jest-opentelemetry/commit/ab17a91601ff8643ed99a6e2adaced491ab0743d)) 96 | 97 | ## [0.1.6](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.5...v0.1.6) (2023-02-21) 98 | 99 | ### Bug Fixes 100 | 101 | - npm publish files ([02afb0b](https://github.com/traceloop/jest-opentelemetry/commit/02afb0b6f6054d3570d2160d667f05033243401d)) 102 | 103 | ## [0.1.2](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.1...v0.1.2) (2023-02-21) 104 | 105 | **Note:** Version bump only for package @traceloop/jest-environment-otel 106 | 107 | ## [0.1.1](https://github.com/expect-dev/jest-opentelemetry/compare/v0.1.0...v0.1.1) (2023-02-21) 108 | 109 | **Note:** Version bump only for package @traceloop/jest-environment-otel 110 | 111 | # 0.1.0 (2023-02-21) 112 | 113 | ### Bug Fixes 114 | 115 | - compilation & tests ([4e8698f](https://github.com/expect-dev/jest-opentelemetry/commit/4e8698f254bad209becf2bd260679831fb25c0a6)) 116 | - imports from dist ([#16](https://github.com/expect-dev/jest-opentelemetry/issues/16)) ([230849d](https://github.com/expect-dev/jest-opentelemetry/commit/230849d617887d53b85a012cd1878dbdfca19e7b)) 117 | - **integration:** integration test dsl with test servers ([#4](https://github.com/expect-dev/jest-opentelemetry/issues/4)) ([6b4b7f7](https://github.com/expect-dev/jest-opentelemetry/commit/6b4b7f7c43f7f450b615bf46635a817df708517e)) 118 | - **lint:** linting issues ([#11](https://github.com/expect-dev/jest-opentelemetry/issues/11)) ([c74ca77](https://github.com/expect-dev/jest-opentelemetry/commit/c74ca77896c35a8ff72500451d41cc0c53d1c8b1)) 119 | - otel receiver timeouts ([#17](https://github.com/expect-dev/jest-opentelemetry/issues/17)) ([8c99815](https://github.com/expect-dev/jest-opentelemetry/commit/8c998151a7a30c1a401aa1403587f35026d4814b)) 120 | - typescript adaptations ([ec0ad8e](https://github.com/expect-dev/jest-opentelemetry/commit/ec0ad8e40c799f70881ae7ac3b441c7f6eb6c9cd)) 121 | 122 | ### Features 123 | 124 | - basic integration with otel-receiver ([10f4a4e](https://github.com/expect-dev/jest-opentelemetry/commit/10f4a4e274079620f8461eb64b09c73e9a609077)) 125 | -------------------------------------------------------------------------------- /packages/jest-environment-otel/README.md: -------------------------------------------------------------------------------- 1 | # jest-environment-otel 2 | 3 | Run your tests using Jest & OpenTelemetry 🎪✨ 4 | 5 | ``` 6 | npm install @traceloop/jest-environment-otel 7 | ``` 8 | 9 | ## Usage 10 | 11 | Update your Jest configuration: 12 | 13 | ```json 14 | { 15 | "globalSetup": "@traceloop/jest-environment-otel/setup", 16 | "globalTeardown": "@traceloop/jest-environment-otel/teardown", 17 | "testEnvironment": "@traceloop/jest-environment-otel" 18 | } 19 | ``` 20 | 21 | ## License 22 | 23 | Apache-2.0 24 | -------------------------------------------------------------------------------- /packages/jest-environment-otel/index.js: -------------------------------------------------------------------------------- 1 | import { teardown, setup } from './dist/global'; 2 | import { OpenTelemetryEnvironment } from './dist/env'; 3 | 4 | export default OpenTelemetryEnvironment; 5 | export { setup, teardown }; 6 | -------------------------------------------------------------------------------- /packages/jest-environment-otel/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '/../jest-opentelemetry', 3 | transform: { 4 | '^.+\\.(t|j)sx?$': ['@swc/jest'], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/jest-environment-otel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@traceloop/jest-environment-otel", 3 | "description": "OpenTelemetry environment for Jest.", 4 | "version": "0.8.0", 5 | "license": "Apache-2.0", 6 | "type": "commonjs", 7 | "main": "./index.js", 8 | "exports": { 9 | ".": "./index.js", 10 | "./setup": "./setup.js", 11 | "./teardown": "./teardown.js", 12 | "./package.json": "./package.json" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/traceloop/jest-opentelemetry.git", 17 | "directory": "packages/jest-environment-otel" 18 | }, 19 | "homepage": "https://github.com/traceloop/jest-opentelemetry/tree/main/packages/jest-environment-otel#readme", 20 | "bugs": { 21 | "url": "https://github.com/traceloop/jest-opentelemetry/issues" 22 | }, 23 | "engines": { 24 | "node": ">=14.0.0" 25 | }, 26 | "keywords": [ 27 | "jest", 28 | "jest-environment", 29 | "opentelemetry" 30 | ], 31 | "publishConfig": { 32 | "access": "public" 33 | }, 34 | "scripts": { 35 | "prebuild": "rm -rf dist", 36 | "build": "rollup -c" 37 | }, 38 | "devDependencies": { 39 | "@types/cwd": "^0.10.0", 40 | "@types/jest-dev-server": "^5.0.0", 41 | "@types/merge-deep": "^3.0.0", 42 | "rollup": "^3.20.0", 43 | "rollup-plugin-swc3": "^0.8.0" 44 | }, 45 | "dependencies": { 46 | "@traceloop/otel-receiver": "^0.8.0", 47 | "chalk": "^4.1.2", 48 | "cwd": "^0.10.0", 49 | "jest-dev-server": "^7.0.1", 50 | "jest-environment-node": "^29.5.0", 51 | "merge-deep": "^3.0.3" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/jest-environment-otel/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { swc, defineRollupSwcOption } from 'rollup-plugin-swc3'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | 4 | const bundle = (config) => ({ 5 | external: (id) => { 6 | return !/^[./]/.test(id); 7 | }, 8 | ...config, 9 | }); 10 | 11 | const swcPlugin = swc( 12 | defineRollupSwcOption({ 13 | jsc: { target: 'es2021' }, 14 | }), 15 | ); 16 | 17 | export default [ 18 | bundle({ 19 | input: 'src/global.ts', 20 | output: { 21 | file: 'dist/global.js', 22 | format: 'cjs', 23 | interop: 'compat', 24 | }, 25 | plugins: [swcPlugin, typescript({ tsconfig: '../../tsconfig.json' })], 26 | }), 27 | bundle({ 28 | input: 'src/env.js', 29 | output: { 30 | file: 'dist/env.js', 31 | format: 'cjs', 32 | interop: 'compat', 33 | }, 34 | plugins: [swcPlugin, typescript({ tsconfig: '../../tsconfig.json' })], 35 | }), 36 | ]; 37 | -------------------------------------------------------------------------------- /packages/jest-environment-otel/setup.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | module.exports = require('./dist/global').setup; 3 | -------------------------------------------------------------------------------- /packages/jest-environment-otel/src/env.js: -------------------------------------------------------------------------------- 1 | import NodeEnvironment from 'jest-environment-node'; 2 | import { readConfig } from './readConfig'; 3 | 4 | const handleError = (error) => { 5 | process.emit('uncaughtException', error); 6 | }; 7 | 8 | const KEYS = { 9 | CONTROL_C: '\u0003', 10 | CONTROL_D: '\u0004', 11 | ENTER: '\r', 12 | }; 13 | // JEST_WORKER_ID starts at 1 14 | const getWorkerIndex = () => process.env.JEST_WORKER_ID - 1; 15 | 16 | // const screenshotsDirectory = join(process.cwd(), "screenshots"); 17 | 18 | class OpenTelemetryEnvironment extends NodeEnvironment { 19 | // Jest is not available here, so we have to reverse engineer 20 | // the setTimeout function, see https://github.com/facebook/jest/blob/v23.1.0/packages/jest-runtime/src/index.js#L823 21 | setTimeout(timeout) { 22 | if (this.global.jasmine) { 23 | // eslint-disable-next-line no-underscore-dangle 24 | this.global.jasmine.DEFAULT_TIMEOUT_INTERVAL = timeout; 25 | } else { 26 | this.global[Symbol.for('TEST_TIMEOUT_SYMBOL')] = timeout; 27 | } 28 | } 29 | 30 | async setup() { 31 | const config = await readConfig(); 32 | this.global.openTelemetryConfig = config; 33 | 34 | this.global.jestOpenTelemetry = {}; 35 | } 36 | 37 | // eslint-disable-next-line @typescript-eslint/no-empty-function 38 | async teardown() {} 39 | } 40 | 41 | export { OpenTelemetryEnvironment }; 42 | -------------------------------------------------------------------------------- /packages/jest-environment-otel/src/global.ts: -------------------------------------------------------------------------------- 1 | import { 2 | setup as setupServer, 3 | teardown as teardownServer, 4 | ERROR_TIMEOUT, 5 | } from 'jest-dev-server'; 6 | import chalk from 'chalk'; 7 | import { readConfig } from './readConfig'; 8 | import type { Config as JestConfig } from 'jest'; 9 | 10 | let didAlreadyRunInWatchMode = false; 11 | 12 | export async function setup(jestConfig: JestConfig = {}) { 13 | const config = await readConfig(); 14 | 15 | // If we are in watch mode, - only setupServer() once. 16 | if (jestConfig.watch || jestConfig.watchAll) { 17 | if (didAlreadyRunInWatchMode) return; 18 | didAlreadyRunInWatchMode = true; 19 | } 20 | 21 | if (!config?.useLocalOtelReceiver) { 22 | return; 23 | } 24 | 25 | try { 26 | await setupServer({ 27 | command: 'node ./node_modules/@traceloop/otel-receiver/dist/index.js', 28 | // debug: true, 29 | host: 'localhost', 30 | port: 4123, 31 | protocol: 'http', 32 | path: 'ping', 33 | usedPortAction: 'kill', // todo: improve this 34 | }); 35 | } catch (error: any) { 36 | if (error.code === ERROR_TIMEOUT) { 37 | console.log(''); 38 | console.error(chalk.red(error.message)); 39 | process.exit(1); 40 | } 41 | throw error; 42 | } 43 | } 44 | 45 | export async function teardown(jestConfig: JestConfig = {}) { 46 | if (!jestConfig.watch && !jestConfig.watchAll) { 47 | await teardownServer(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/jest-environment-otel/src/readConfig.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import { promisify } from 'util'; 4 | import cwd from 'cwd'; 5 | import merge from 'merge-deep'; 6 | import { setTimeout } from 'timers/promises'; 7 | 8 | const exists = promisify(fs.exists); 9 | 10 | const DEFAULT_CONFIG = { 11 | launch: {}, 12 | }; 13 | const DEFAULT_CONFIG_CI = merge(DEFAULT_CONFIG, { 14 | launch: { 15 | args: [], 16 | }, 17 | }); 18 | 19 | export async function readConfig() { 20 | const defaultConfig = 21 | process.env.CI === 'true' ? DEFAULT_CONFIG_CI : DEFAULT_CONFIG; 22 | 23 | const hasCustomConfigPath = !!process.env.JEST_OPENTELEMETRY_CONFIG; 24 | const configPath = 25 | process.env.JEST_OPENTELEMETRY_CONFIG || 'jest-opentelemetry.config.js'; 26 | const absConfigPath = path.resolve(cwd(), configPath); 27 | const configExists = await exists(absConfigPath); 28 | 29 | if (hasCustomConfigPath && !configExists) { 30 | throw new Error( 31 | `Error: Can't find a root directory while resolving a config file path.\nProvided path to resolve: ${configPath}`, 32 | ); 33 | } 34 | 35 | if (!hasCustomConfigPath && !configExists) { 36 | return defaultConfig; 37 | } 38 | 39 | const localConfig = await require(absConfigPath); 40 | 41 | if (localConfig.timeout) { 42 | await setTimeout(localConfig.timeout); 43 | } 44 | 45 | return merge({}, defaultConfig, localConfig); 46 | } 47 | -------------------------------------------------------------------------------- /packages/jest-environment-otel/teardown.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | module.exports = require('./dist/global').teardown; 3 | -------------------------------------------------------------------------------- /packages/jest-opentelemetry/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [0.8.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.7.0...v0.8.0) (2023-06-20) 7 | 8 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 9 | 10 | # [0.7.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.6.0...v0.7.0) (2023-06-13) 11 | 12 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 13 | 14 | # [0.6.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.5.0...v0.6.0) (2023-06-04) 15 | 16 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 17 | 18 | # [0.5.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.4.1...v0.5.0) (2023-03-17) 19 | 20 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 21 | 22 | # 0.4.0 (2023-02-22) 23 | 24 | ### Bug Fixes 25 | 26 | - compilation & tests ([4e8698f](https://github.com/traceloop/jest-opentelemetry/commit/4e8698f254bad209becf2bd260679831fb25c0a6)) 27 | - **expect:** type exports ([#21](https://github.com/traceloop/jest-opentelemetry/issues/21)) ([85f6481](https://github.com/traceloop/jest-opentelemetry/commit/85f6481501c5018a4e1a717f6cd8221deb2ab1f1)) 28 | - jest preset references ([2665c1a](https://github.com/traceloop/jest-opentelemetry/commit/2665c1a0f301d0f0df8979a64c8fdfeea367dbc4)) 29 | - package references ([029cfc9](https://github.com/traceloop/jest-opentelemetry/commit/029cfc9815403487a25dd780e26d92900378b66d)) 30 | 31 | # [0.3.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.7...v0.3.0) (2023-02-21) 32 | 33 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 34 | 35 | ## [0.2.7](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.6...v0.2.7) (2023-02-21) 36 | 37 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 38 | 39 | ## [0.2.6](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.5...v0.2.6) (2023-02-21) 40 | 41 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 42 | 43 | ## [0.2.5](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.4...v0.2.5) (2023-02-21) 44 | 45 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 46 | 47 | ## [0.2.4](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.3...v0.2.4) (2023-02-21) 48 | 49 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 50 | 51 | ## [0.2.3](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.2...v0.2.3) (2023-02-21) 52 | 53 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 54 | 55 | ## [0.2.2](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.1...v0.2.2) (2023-02-21) 56 | 57 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 58 | 59 | ## [0.2.1](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.0...v0.2.1) (2023-02-21) 60 | 61 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 62 | 63 | # [0.2.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.8...v0.2.0) (2023-02-21) 64 | 65 | ### Bug Fixes 66 | 67 | - **expect:** type exports ([#21](https://github.com/traceloop/jest-opentelemetry/issues/21)) ([85f6481](https://github.com/traceloop/jest-opentelemetry/commit/85f6481501c5018a4e1a717f6cd8221deb2ab1f1)) 68 | 69 | ## [0.1.10](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.9...v0.1.10) (2023-02-21) 70 | 71 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 72 | 73 | ## [0.1.9](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.8...v0.1.9) (2023-02-21) 74 | 75 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 76 | 77 | ## [0.1.8](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.7...v0.1.8) (2023-02-21) 78 | 79 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 80 | 81 | ## [0.1.7](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.6...v0.1.7) (2023-02-21) 82 | 83 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 84 | 85 | ## [0.1.6](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.5...v0.1.6) (2023-02-21) 86 | 87 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 88 | 89 | ## [0.1.5](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.4...v0.1.5) (2023-02-21) 90 | 91 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 92 | 93 | ## [0.1.4](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.3...v0.1.4) (2023-02-21) 94 | 95 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 96 | 97 | ## [0.1.3](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.2...v0.1.3) (2023-02-21) 98 | 99 | ### Bug Fixes 100 | 101 | - package references ([029cfc9](https://github.com/traceloop/jest-opentelemetry/commit/029cfc9815403487a25dd780e26d92900378b66d)) 102 | 103 | ## [0.1.2](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.1...v0.1.2) (2023-02-21) 104 | 105 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 106 | 107 | ## [0.1.1](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.0...v0.1.1) (2023-02-21) 108 | 109 | **Note:** Version bump only for package @traceloop/jest-opentelemetry 110 | 111 | # 0.1.0 (2023-02-21) 112 | 113 | ### Bug Fixes 114 | 115 | - compilation & tests ([4e8698f](https://github.com/traceloop/jest-opentelemetry/commit/4e8698f254bad209becf2bd260679831fb25c0a6)) 116 | - jest preset references ([2665c1a](https://github.com/traceloop/jest-opentelemetry/commit/2665c1a0f301d0f0df8979a64c8fdfeea367dbc4)) 117 | -------------------------------------------------------------------------------- /packages/jest-opentelemetry/README.md: -------------------------------------------------------------------------------- 1 | # jest-otel 2 | 3 | Jest preset containing all required configuration for writing integration tests using OpenTelemetry. 4 | 5 | ``` 6 | npm install @traceloop/jest-opentelemetry 7 | ``` 8 | 9 | ## Usage 10 | 11 | ```js 12 | // jest.config.js 13 | module.exports = { 14 | preset: '@traceloop/jest-opentelemetry', 15 | }; 16 | ``` 17 | 18 | ## License 19 | 20 | Apache-2.0 21 | -------------------------------------------------------------------------------- /packages/jest-opentelemetry/jest-preset.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | globalSetup: require.resolve('@traceloop/jest-environment-otel/setup'), 3 | globalTeardown: require.resolve('@traceloop/jest-environment-otel/teardown'), 4 | testEnvironment: require.resolve('@traceloop/jest-environment-otel'), 5 | setupFilesAfterEnv: [require.resolve('@traceloop/expect-opentelemetry')], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/jest-opentelemetry/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@traceloop/jest-opentelemetry", 3 | "description": "Run your tests using Jest & OpenTelemetry.", 4 | "version": "0.8.0", 5 | "license": "Apache-2.0", 6 | "type": "commonjs", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/traceloop/jest-opentelemetry.git", 10 | "directory": "packages/jest-opentelemetry" 11 | }, 12 | "homepage": "https://github.com/traceloop/jest-opentelemetry/tree/main/packages/jest-opentelemetry#readme", 13 | "bugs": { 14 | "url": "https://github.com/traceloop/jest-opentelemetry/issues" 15 | }, 16 | "engines": { 17 | "node": ">=14.0.0" 18 | }, 19 | "publishConfig": { 20 | "access": "public" 21 | }, 22 | "keywords": [ 23 | "jest", 24 | "opentelemetry", 25 | "jest-opentelemetry" 26 | ], 27 | "dependencies": { 28 | "@traceloop/expect-opentelemetry": "^0.8.0", 29 | "@traceloop/jest-environment-otel": "^0.8.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/otel-proto/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [0.8.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.7.0...v0.8.0) (2023-06-20) 7 | 8 | **Note:** Version bump only for package @traceloop/otel-proto 9 | 10 | 11 | 12 | 13 | 14 | # [0.7.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.6.0...v0.7.0) (2023-06-13) 15 | 16 | **Note:** Version bump only for package @traceloop/otel-proto 17 | 18 | 19 | 20 | 21 | 22 | # [0.6.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.5.0...v0.6.0) (2023-06-04) 23 | 24 | **Note:** Version bump only for package @traceloop/otel-proto 25 | 26 | 27 | 28 | 29 | 30 | # [0.5.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.4.1...v0.5.0) (2023-03-17) 31 | 32 | 33 | ### Features 34 | 35 | * traceloop proto ([#52](https://github.com/traceloop/jest-opentelemetry/issues/52)) ([53e9e68](https://github.com/traceloop/jest-opentelemetry/commit/53e9e68ab34bf79ab52adb82be219d48fef64acb)) 36 | 37 | 38 | 39 | 40 | 41 | # 0.4.0 (2023-02-22) 42 | 43 | ### Bug Fixes 44 | 45 | - created a separate package for protobuf code ([b445f6f](https://github.com/traceloop/jest-opentelemetry/commit/b445f6fb7a32989b44253d813c4f92ad5dfffd2f)) 46 | - moved dependncies to packages that need them ([60dc5ae](https://github.com/traceloop/jest-opentelemetry/commit/60dc5ae524920f9efea717034ae64152a180c69b)) 47 | 48 | # [0.3.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.7...v0.3.0) (2023-02-21) 49 | 50 | **Note:** Version bump only for package @traceloop/otel-proto 51 | 52 | ## [0.2.7](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.6...v0.2.7) (2023-02-21) 53 | 54 | ### Bug Fixes 55 | 56 | - moved dependncies to packages that need them ([60dc5ae](https://github.com/traceloop/jest-opentelemetry/commit/60dc5ae524920f9efea717034ae64152a180c69b)) 57 | 58 | ## [0.2.6](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.5...v0.2.6) (2023-02-21) 59 | 60 | ### Bug Fixes 61 | 62 | - created a separate package for protobuf code ([b445f6f](https://github.com/traceloop/jest-opentelemetry/commit/b445f6fb7a32989b44253d813c4f92ad5dfffd2f)) 63 | -------------------------------------------------------------------------------- /packages/otel-proto/README.md: -------------------------------------------------------------------------------- 1 | # otel-proto 2 | 3 | This repository contains both otel proto + traceloop proto files compiled to JS with type definitions 4 | 5 | ## Compiling proto files: 6 | Compile JS: 7 | npx pbjs -t static-module -w commonjs -o src/index.js --es6 src/opentelemetry/proto/trace/v1/trace.proto src/opentelemetry/proto/resource/v1/resource.proto src/opentelemetry/proto/common/v1/common.proto src/traceloop/proto/v1/traceloop.proto 8 | 9 | Create type definitions: 10 | npx pbts -o src/index.d.ts src/index.js -------------------------------------------------------------------------------- /packages/otel-proto/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@traceloop/otel-proto", 3 | "description": "Otel & Traceloop proto compiled to JS with type definitions", 4 | "version": "0.8.0", 5 | "license": "Apache-2.0", 6 | "type": "commonjs", 7 | "main": "./dist/index.js", 8 | "types": "./dist/index.d.ts", 9 | "exports": { 10 | ".": { 11 | "require": "./dist/index.js", 12 | "types": "./dist/index.d.ts" 13 | }, 14 | "./package.json": "./package.json" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/traceloop/jest-opentelemetry.git", 19 | "directory": "packages/otel-proto" 20 | }, 21 | "homepage": "https://github.com/traceloop/jest-opentelemetry/tree/main/packages/otel-proto#readme", 22 | "bugs": { 23 | "url": "https://github.com/traceloop/jest-opentelemetry/issues" 24 | }, 25 | "engines": { 26 | "node": ">=14.0.0" 27 | }, 28 | "keywords": [ 29 | "jest", 30 | "opentelemetry", 31 | "jest-opentelemetry", 32 | "expect", 33 | "assert", 34 | "should", 35 | "assertion" 36 | ], 37 | "publishConfig": { 38 | "access": "public" 39 | }, 40 | "scripts": { 41 | "prebuild": "rm -rf dist", 42 | "build": "rollup -c" 43 | }, 44 | "dependencies": { 45 | "express": "^4.18.2", 46 | "protobufjs": "^7.2.2" 47 | }, 48 | "devDependencies": { 49 | "rollup-plugin-copy": "^3.4.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/otel-proto/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { swc, defineRollupSwcOption } from 'rollup-plugin-swc3'; 2 | import copy from 'rollup-plugin-copy'; 3 | 4 | const bundle = (config) => ({ 5 | external: (id) => { 6 | return !/^[./]/.test(id); 7 | }, 8 | ...config, 9 | }); 10 | 11 | const swcPlugin = swc( 12 | defineRollupSwcOption({ 13 | jsc: { target: 'es2021' }, 14 | }), 15 | ); 16 | 17 | export default [ 18 | bundle({ 19 | input: 'src/index.js', 20 | output: { 21 | file: 'dist/index.js', 22 | format: 'cjs', 23 | }, 24 | plugins: [ 25 | swcPlugin, 26 | copy({ targets: [{ src: 'src/index.d.ts', dest: 'dist' }] }), 27 | ], 28 | }), 29 | ]; 30 | -------------------------------------------------------------------------------- /packages/otel-proto/src/opentelemetry/proto/common/v1/common.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2019, OpenTelemetry Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package opentelemetry.proto.common.v1; 18 | 19 | option csharp_namespace = "OpenTelemetry.Proto.Common.V1"; 20 | option java_multiple_files = true; 21 | option java_package = "io.opentelemetry.proto.common.v1"; 22 | option java_outer_classname = "CommonProto"; 23 | option go_package = "go.opentelemetry.io/proto/otlp/common/v1"; 24 | 25 | // AnyValue is used to represent any type of attribute value. AnyValue may contain a 26 | // primitive value such as a string or integer or it may contain an arbitrary nested 27 | // object containing arrays, key-value lists and primitives. 28 | message AnyValue { 29 | // The value is one of the listed fields. It is valid for all values to be unspecified 30 | // in which case this AnyValue is considered to be "empty". 31 | oneof value { 32 | string string_value = 1; 33 | bool bool_value = 2; 34 | int64 int_value = 3; 35 | double double_value = 4; 36 | ArrayValue array_value = 5; 37 | KeyValueList kvlist_value = 6; 38 | bytes bytes_value = 7; 39 | } 40 | } 41 | 42 | // ArrayValue is a list of AnyValue messages. We need ArrayValue as a message 43 | // since oneof in AnyValue does not allow repeated fields. 44 | message ArrayValue { 45 | // Array of values. The array may be empty (contain 0 elements). 46 | repeated AnyValue values = 1; 47 | } 48 | 49 | // KeyValueList is a list of KeyValue messages. We need KeyValueList as a message 50 | // since `oneof` in AnyValue does not allow repeated fields. Everywhere else where we need 51 | // a list of KeyValue messages (e.g. in Span) we use `repeated KeyValue` directly to 52 | // avoid unnecessary extra wrapping (which slows down the protocol). The 2 approaches 53 | // are semantically equivalent. 54 | message KeyValueList { 55 | // A collection of key/value pairs of key-value pairs. The list may be empty (may 56 | // contain 0 elements). 57 | // The keys MUST be unique (it is not allowed to have more than one 58 | // value with the same key). 59 | repeated KeyValue values = 1; 60 | } 61 | 62 | // KeyValue is a key-value pair that is used to store Span attributes, Link 63 | // attributes, etc. 64 | message KeyValue { 65 | string key = 1; 66 | AnyValue value = 2; 67 | } 68 | 69 | // InstrumentationScope is a message representing the instrumentation scope information 70 | // such as the fully qualified name and version. 71 | message InstrumentationScope { 72 | // An empty instrumentation scope name means the name is unknown. 73 | string name = 1; 74 | string version = 2; 75 | repeated KeyValue attributes = 3; 76 | uint32 dropped_attributes_count = 4; 77 | } 78 | -------------------------------------------------------------------------------- /packages/otel-proto/src/opentelemetry/proto/resource/v1/resource.proto: -------------------------------------------------------------------------------- 1 | // Copyright 2019, OpenTelemetry Authors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | syntax = "proto3"; 16 | 17 | package opentelemetry.proto.resource.v1; 18 | 19 | import "opentelemetry/proto/common/v1/common.proto"; 20 | 21 | option csharp_namespace = "OpenTelemetry.Proto.Resource.V1"; 22 | option java_multiple_files = true; 23 | option java_package = "io.opentelemetry.proto.resource.v1"; 24 | option java_outer_classname = "ResourceProto"; 25 | option go_package = "go.opentelemetry.io/proto/otlp/resource/v1"; 26 | 27 | // Resource information. 28 | message Resource { 29 | // Set of attributes that describe the resource. 30 | // Attribute keys MUST be unique (it is not allowed to have more than one 31 | // attribute with the same key). 32 | repeated opentelemetry.proto.common.v1.KeyValue attributes = 1; 33 | 34 | // dropped_attributes_count is the number of dropped attributes. If the value is 0, then 35 | // no attributes were dropped. 36 | uint32 dropped_attributes_count = 2; 37 | } 38 | -------------------------------------------------------------------------------- /packages/otel-proto/src/traceloop/proto/v1/traceloop.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | package traceloop.proto.traceloop.v1; 4 | 5 | import "opentelemetry/proto/common/v1/common.proto"; 6 | import "opentelemetry/proto/resource/v1/resource.proto"; 7 | import "opentelemetry/proto/trace/v1/trace.proto"; 8 | 9 | option go_package = "./"; 10 | 11 | message TraceloopSpan { 12 | // The resource for the span. 13 | opentelemetry.proto.resource.v1.Resource resource = 1; 14 | 15 | // The instrumentation scope for the span. 16 | opentelemetry.proto.common.v1.InstrumentationScope scope = 2; 17 | 18 | // The span. 19 | opentelemetry.proto.trace.v1.Span span = 3; 20 | 21 | string trace_id = 4; 22 | 23 | string span_id = 5; 24 | 25 | string parent_span_id = 6; 26 | 27 | string customer_id = 7; 28 | 29 | string traceloop_id = 8; 30 | } 31 | -------------------------------------------------------------------------------- /packages/otel-receiver/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # [0.8.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.7.0...v0.8.0) (2023-06-20) 7 | 8 | **Note:** Version bump only for package @traceloop/otel-receiver 9 | 10 | # [0.7.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.6.0...v0.7.0) (2023-06-13) 11 | 12 | **Note:** Version bump only for package @traceloop/otel-receiver 13 | 14 | # [0.6.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.5.0...v0.6.0) (2023-06-04) 15 | 16 | **Note:** Version bump only for package @traceloop/otel-receiver 17 | 18 | # [0.5.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.4.1...v0.5.0) (2023-03-17) 19 | 20 | ### Features 21 | 22 | - go server backend ([#42](https://github.com/traceloop/jest-opentelemetry/issues/42)) ([a543845](https://github.com/traceloop/jest-opentelemetry/commit/a543845445617bd321c7cee793e23caf2c651844)) 23 | 24 | # 0.4.0 (2023-02-22) 25 | 26 | ### Bug Fixes 27 | 28 | - compilation & tests ([4e8698f](https://github.com/traceloop/jest-opentelemetry/commit/4e8698f254bad209becf2bd260679831fb25c0a6)) 29 | - created a separate package for protobuf code ([b445f6f](https://github.com/traceloop/jest-opentelemetry/commit/b445f6fb7a32989b44253d813c4f92ad5dfffd2f)) 30 | - import paths of proto package ([466e8b1](https://github.com/traceloop/jest-opentelemetry/commit/466e8b13521f77d4d1893b86b438f488a8ade0e7)) 31 | - imports from dist ([#16](https://github.com/traceloop/jest-opentelemetry/issues/16)) ([230849d](https://github.com/traceloop/jest-opentelemetry/commit/230849d617887d53b85a012cd1878dbdfca19e7b)) 32 | - **lint:** linting issues ([#11](https://github.com/traceloop/jest-opentelemetry/issues/11)) ([c74ca77](https://github.com/traceloop/jest-opentelemetry/commit/c74ca77896c35a8ff72500451d41cc0c53d1c8b1)) 33 | - otel receiver timeouts ([#17](https://github.com/traceloop/jest-opentelemetry/issues/17)) ([8c99815](https://github.com/traceloop/jest-opentelemetry/commit/8c998151a7a30c1a401aa1403587f35026d4814b)) 34 | - otel-receiver can be executed ([4360cdb](https://github.com/traceloop/jest-opentelemetry/commit/4360cdb3970835142c35f3eb28d7355f909a165b)) 35 | - typescript adaptations ([ec0ad8e](https://github.com/traceloop/jest-opentelemetry/commit/ec0ad8e40c799f70881ae7ac3b441c7f6eb6c9cd)) 36 | 37 | ### Features 38 | 39 | - basic integration with otel-receiver ([10f4a4e](https://github.com/traceloop/jest-opentelemetry/commit/10f4a4e274079620f8461eb64b09c73e9a609077)) 40 | 41 | # [0.3.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.7...v0.3.0) (2023-02-21) 42 | 43 | ### Bug Fixes 44 | 45 | - import paths of proto package ([466e8b1](https://github.com/traceloop/jest-opentelemetry/commit/466e8b13521f77d4d1893b86b438f488a8ade0e7)) 46 | 47 | ## [0.2.7](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.6...v0.2.7) (2023-02-21) 48 | 49 | **Note:** Version bump only for package @traceloop/otel-receiver 50 | 51 | ## [0.2.6](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.5...v0.2.6) (2023-02-21) 52 | 53 | ### Bug Fixes 54 | 55 | - created a separate package for protobuf code ([b445f6f](https://github.com/traceloop/jest-opentelemetry/commit/b445f6fb7a32989b44253d813c4f92ad5dfffd2f)) 56 | 57 | ## [0.2.5](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.4...v0.2.5) (2023-02-21) 58 | 59 | ### Bug Fixes 60 | 61 | - otel-receiver can be executed ([4360cdb](https://github.com/traceloop/jest-opentelemetry/commit/4360cdb3970835142c35f3eb28d7355f909a165b)) 62 | 63 | # [0.2.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.8...v0.2.0) (2023-02-21) 64 | 65 | **Note:** Version bump only for package @traceloop/otel-receiver 66 | 67 | ## [0.1.2](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.1...v0.1.2) (2023-02-21) 68 | 69 | **Note:** Version bump only for package @traceloop/otel-receiver 70 | 71 | ## [0.1.1](https://github.com/expect-dev/jest-opentelemetry/compare/v0.1.0...v0.1.1) (2023-02-21) 72 | 73 | **Note:** Version bump only for package @traceloop/otel-receiver 74 | 75 | # 0.1.0 (2023-02-21) 76 | 77 | ### Bug Fixes 78 | 79 | - compilation & tests ([4e8698f](https://github.com/expect-dev/jest-opentelemetry/commit/4e8698f254bad209becf2bd260679831fb25c0a6)) 80 | - imports from dist ([#16](https://github.com/expect-dev/jest-opentelemetry/issues/16)) ([230849d](https://github.com/expect-dev/jest-opentelemetry/commit/230849d617887d53b85a012cd1878dbdfca19e7b)) 81 | - **lint:** linting issues ([#11](https://github.com/expect-dev/jest-opentelemetry/issues/11)) ([c74ca77](https://github.com/expect-dev/jest-opentelemetry/commit/c74ca77896c35a8ff72500451d41cc0c53d1c8b1)) 82 | - otel receiver timeouts ([#17](https://github.com/expect-dev/jest-opentelemetry/issues/17)) ([8c99815](https://github.com/expect-dev/jest-opentelemetry/commit/8c998151a7a30c1a401aa1403587f35026d4814b)) 83 | - typescript adaptations ([ec0ad8e](https://github.com/expect-dev/jest-opentelemetry/commit/ec0ad8e40c799f70881ae7ac3b441c7f6eb6c9cd)) 84 | 85 | ### Features 86 | 87 | - basic integration with otel-receiver ([10f4a4e](https://github.com/expect-dev/jest-opentelemetry/commit/10f4a4e274079620f8461eb64b09c73e9a609077)) 88 | -------------------------------------------------------------------------------- /packages/otel-receiver/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 [yyyy] [name of copyright owner] 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 | -------------------------------------------------------------------------------- /packages/otel-receiver/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@traceloop/otel-receiver", 3 | "description": "A node server to receive and cache incoming opentelemetry spans.", 4 | "version": "0.8.0", 5 | "license": "Apache-2.0", 6 | "type": "commonjs", 7 | "main": "./dist/index.js", 8 | "bin": "./dist/index.js", 9 | "exports": { 10 | ".": "./dist/index.js", 11 | "./package.json": "./package.json" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "https://github.com/traceloop/jest-opentelemetry.git", 16 | "directory": "packages/otel-receiver" 17 | }, 18 | "homepage": "https://github.com/traceloop/jest-opentelemetry/tree/main/packages/otel-receiver#readme", 19 | "bugs": { 20 | "url": "https://github.com/traceloop/jest-opentelemetry/issues" 21 | }, 22 | "engines": { 23 | "node": ">=14.0.0" 24 | }, 25 | "keywords": [ 26 | "jest", 27 | "opentelemetry", 28 | "jest-opentelemetry", 29 | "expect", 30 | "assert", 31 | "should", 32 | "assertion" 33 | ], 34 | "publishConfig": { 35 | "access": "public" 36 | }, 37 | "scripts": { 38 | "prebuild": "rm -rf dist", 39 | "build": "rollup -c", 40 | "start": "node ./dist/index.js" 41 | }, 42 | "devDependencies": { 43 | "@types/express": "^4.17.17", 44 | "rollup": "^3.20.0", 45 | "rollup-plugin-swc3": "^0.8.0" 46 | }, 47 | "dependencies": { 48 | "@traceloop/otel-proto": "^0.8.0", 49 | "express": "^4.18.2" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/otel-receiver/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { swc, defineRollupSwcOption } from 'rollup-plugin-swc3'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | 4 | const bundle = (config) => ({ 5 | external: (id) => { 6 | return !/^[./]/.test(id); 7 | }, 8 | ...config, 9 | }); 10 | 11 | const swcPlugin = swc( 12 | defineRollupSwcOption({ 13 | jsc: { target: 'es2021' }, 14 | }), 15 | ); 16 | 17 | export default [ 18 | bundle({ 19 | input: 'src/index.ts', 20 | output: { 21 | file: 'dist/index.js', 22 | format: 'cjs', 23 | }, 24 | plugins: [swcPlugin, typescript({ tsconfig: '../../tsconfig.json' })], 25 | }), 26 | ]; 27 | -------------------------------------------------------------------------------- /packages/otel-receiver/src/index.ts: -------------------------------------------------------------------------------- 1 | import express, { Request, Response } from 'express'; 2 | import { addToStore, getAll } from './store'; 3 | import { opentelemetry } from '@traceloop/otel-proto'; 4 | 5 | const app = express(); 6 | const port = 4123; 7 | let _server; 8 | 9 | const startServer = async () => { 10 | app.post( 11 | '/v1/traces', 12 | express.raw({ type: '*/*' }), 13 | (req: Request, res: Response) => { 14 | const tracesData = opentelemetry.proto.trace.v1.TracesData.decode( 15 | req.body, 16 | ); 17 | 18 | addToStore(tracesData.resourceSpans); 19 | 20 | res.send(); 21 | }, 22 | ); 23 | 24 | app.get('/v1/traces/:traceloopId', (_: Request, res: Response) => { 25 | res.send(getAll()); 26 | }); 27 | 28 | app.get('/ping', (_: Request, res: Response) => { 29 | res.send('pong'); 30 | }); 31 | 32 | _server = app.listen(port, () => { 33 | console.log(`otel-receiver listening at port ${port}`); 34 | }); 35 | }; 36 | 37 | const gracefulShutdownHandler = function gracefulShutdownHandler(signal) { 38 | console.log(`otel-receiver caught ${signal}, gracefully shutting down`); 39 | 40 | setTimeout(() => { 41 | _server.close(function () { 42 | process.exit(); 43 | }); 44 | }, 0); 45 | }; 46 | 47 | process.on('SIGINT', gracefulShutdownHandler); 48 | process.on('SIGTERM', gracefulShutdownHandler); 49 | 50 | startServer(); 51 | -------------------------------------------------------------------------------- /packages/otel-receiver/src/store.ts: -------------------------------------------------------------------------------- 1 | import { opentelemetry } from '@traceloop/otel-proto'; 2 | 3 | const _resourceSpans: opentelemetry.proto.trace.v1.IResourceSpans[] = []; 4 | 5 | export const addToStore = ( 6 | resourceSpans: opentelemetry.proto.trace.v1.IResourceSpans[], 7 | ) => { 8 | _resourceSpans.push(...resourceSpans); 9 | }; 10 | 11 | export const getAll = () => { 12 | const tracesData = new opentelemetry.proto.trace.v1.TracesData({ 13 | resourceSpans: _resourceSpans, 14 | }); 15 | 16 | return opentelemetry.proto.trace.v1.TracesData.encode(tracesData).finish(); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/test-servers/.dockerignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules -------------------------------------------------------------------------------- /packages/test-servers/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. 5 | 6 | # 0.4.0 (2023-02-22) 7 | 8 | ### Bug Fixes 9 | 10 | - imports from dist ([#16](https://github.com/traceloop/jest-opentelemetry/issues/16)) ([230849d](https://github.com/traceloop/jest-opentelemetry/commit/230849d617887d53b85a012cd1878dbdfca19e7b)) 11 | - **integration:** integration test dsl with test servers ([#4](https://github.com/traceloop/jest-opentelemetry/issues/4)) ([6b4b7f7](https://github.com/traceloop/jest-opentelemetry/commit/6b4b7f7c43f7f450b615bf46635a817df708517e)) 12 | - moved dependncies to packages that need them ([60dc5ae](https://github.com/traceloop/jest-opentelemetry/commit/60dc5ae524920f9efea717034ae64152a180c69b)) 13 | - type definitions ([7fb5242](https://github.com/traceloop/jest-opentelemetry/commit/7fb5242d47a97c8c1e5f496b82492989bb1ef008)) 14 | 15 | ### Features 16 | 17 | - 2 test servers ([0191781](https://github.com/traceloop/jest-opentelemetry/commit/01917812668fd8d31c1962fdefbb539ae3cf7774)) 18 | - basic integration with otel-receiver ([10f4a4e](https://github.com/traceloop/jest-opentelemetry/commit/10f4a4e274079620f8461eb64b09c73e9a609077)) 19 | - **database-matchers:** support postgres matchers ([#24](https://github.com/traceloop/jest-opentelemetry/issues/24)) ([8b86a4b](https://github.com/traceloop/jest-opentelemetry/commit/8b86a4b7c926498c00a3eaa3da326d45eeda8d77)) 20 | - http matchers ([#29](https://github.com/traceloop/jest-opentelemetry/issues/29)) ([9c436c8](https://github.com/traceloop/jest-opentelemetry/commit/9c436c894e08c443404e842c5dbdf40845cd664c)) 21 | - k8s init-container ([c05bda4](https://github.com/traceloop/jest-opentelemetry/commit/c05bda437b9b11e5097e482f6a7885e58789cc5b)) 22 | - kuberneteized test-servers ([5d3283d](https://github.com/traceloop/jest-opentelemetry/commit/5d3283d98779a9fba4321fdafda5bdd5ac5f71c7)) 23 | - otel orchestartion on k8s ([dc19417](https://github.com/traceloop/jest-opentelemetry/commit/dc194173a2c2d7c7a892d437ee0d2101c6ac1605)) 24 | - separate otel instrumentation into a new package ([dfe7586](https://github.com/traceloop/jest-opentelemetry/commit/dfe758613c5e17c4e92144023c82d91526aca786)) 25 | - TraceLoop ([#19](https://github.com/traceloop/jest-opentelemetry/issues/19)) ([ab17a91](https://github.com/traceloop/jest-opentelemetry/commit/ab17a91601ff8643ed99a6e2adaced491ab0743d)) 26 | 27 | # [0.3.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.7...v0.3.0) (2023-02-21) 28 | 29 | **Note:** Version bump only for package @traceloop/test-servers 30 | 31 | ## [0.2.7](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.6...v0.2.7) (2023-02-21) 32 | 33 | ### Bug Fixes 34 | 35 | - moved dependncies to packages that need them ([60dc5ae](https://github.com/traceloop/jest-opentelemetry/commit/60dc5ae524920f9efea717034ae64152a180c69b)) 36 | 37 | ## [0.2.1](https://github.com/traceloop/jest-opentelemetry/compare/v0.2.0...v0.2.1) (2023-02-21) 38 | 39 | ### Bug Fixes 40 | 41 | - type definitions ([7fb5242](https://github.com/traceloop/jest-opentelemetry/commit/7fb5242d47a97c8c1e5f496b82492989bb1ef008)) 42 | 43 | # [0.2.0](https://github.com/traceloop/jest-opentelemetry/compare/v0.1.8...v0.2.0) (2023-02-21) 44 | 45 | ### Features 46 | 47 | - TraceLoop ([#19](https://github.com/traceloop/jest-opentelemetry/issues/19)) ([ab17a91](https://github.com/traceloop/jest-opentelemetry/commit/ab17a91601ff8643ed99a6e2adaced491ab0743d)) 48 | 49 | # 0.1.0 (2023-02-21) 50 | 51 | ### Bug Fixes 52 | 53 | - imports from dist ([#16](https://github.com/traceloop/jest-opentelemetry/issues/16)) ([230849d](https://github.com/traceloop/jest-opentelemetry/commit/230849d617887d53b85a012cd1878dbdfca19e7b)) 54 | - **integration:** integration test dsl with test servers ([#4](https://github.com/traceloop/jest-opentelemetry/issues/4)) ([6b4b7f7](https://github.com/traceloop/jest-opentelemetry/commit/6b4b7f7c43f7f450b615bf46635a817df708517e)) 55 | 56 | ### Features 57 | 58 | - 2 test servers ([0191781](https://github.com/traceloop/jest-opentelemetry/commit/01917812668fd8d31c1962fdefbb539ae3cf7774)) 59 | - basic integration with otel-receiver ([10f4a4e](https://github.com/traceloop/jest-opentelemetry/commit/10f4a4e274079620f8461eb64b09c73e9a609077)) 60 | - kuberneteized test-servers ([5d3283d](https://github.com/traceloop/jest-opentelemetry/commit/5d3283d98779a9fba4321fdafda5bdd5ac5f71c7)) 61 | - otel orchestartion on k8s ([dc19417](https://github.com/traceloop/jest-opentelemetry/commit/dc194173a2c2d7c7a892d437ee0d2101c6ac1605)) 62 | -------------------------------------------------------------------------------- /packages/test-servers/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:16 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | COPY packages/test-servers/package*.json ./packages/test-servers/ 7 | 8 | RUN npm install 9 | 10 | COPY lerna.json ./ 11 | COPY tsconfig.json ./ 12 | COPY packages/test-servers/. ./packages/test-servers/ 13 | 14 | RUN npm run build 15 | 16 | CMD node ./packages/test-servers/dist/index.js -------------------------------------------------------------------------------- /packages/test-servers/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: opentelemetry.io/v1alpha1 2 | kind: Instrumentation 3 | metadata: 4 | name: otel-instrumentation 5 | spec: 6 | nodejs: 7 | image: traceloop/instrument-opentelemetry:node-0.3.0 8 | exporter: 9 | endpoint: http://otel-collector:4317 10 | propagators: 11 | - tracecontext 12 | - baggage 13 | - b3 14 | --- 15 | apiVersion: apps/v1 16 | kind: Deployment 17 | metadata: 18 | name: orders-service 19 | spec: 20 | replicas: 1 21 | selector: 22 | matchLabels: 23 | app: orders-service 24 | template: 25 | metadata: 26 | labels: 27 | app: orders-service 28 | annotations: 29 | instrumentation.opentelemetry.io/inject-nodejs: 'true' 30 | spec: 31 | containers: 32 | - name: orders-service 33 | image: test-servers 34 | ports: 35 | - containerPort: 3000 36 | env: 37 | - name: SERVICE_NAME 38 | value: 'orders-service' 39 | - name: ORDERS_SERVICE 40 | value: 'TRUE' 41 | - name: EMAILS_SERVICE_URL 42 | value: 'http://emails-service:3000' 43 | - name: PORT 44 | value: '3000' 45 | imagePullPolicy: Never 46 | 47 | --- 48 | apiVersion: apps/v1 49 | kind: Deployment 50 | metadata: 51 | name: emails-service 52 | spec: 53 | replicas: 1 54 | selector: 55 | matchLabels: 56 | app: emails-service 57 | template: 58 | metadata: 59 | labels: 60 | app: emails-service 61 | annotations: 62 | instrumentation.opentelemetry.io/inject-nodejs: 'true' 63 | spec: 64 | containers: 65 | - name: emails-service 66 | image: test-servers 67 | ports: 68 | - containerPort: 3000 69 | env: 70 | - name: SERVICE_NAME 71 | value: 'emails-service' 72 | - name: EMAILS_SERVICE 73 | value: 'TRUE' 74 | - name: PORT 75 | value: '3000' 76 | imagePullPolicy: Never 77 | 78 | --- 79 | apiVersion: apps/v1 80 | kind: Deployment 81 | metadata: 82 | name: grpc-service 83 | spec: 84 | replicas: 1 85 | selector: 86 | matchLabels: 87 | app: grpc-service 88 | template: 89 | metadata: 90 | labels: 91 | app: grpc-service 92 | annotations: 93 | instrumentation.opentelemetry.io/inject-nodejs: 'true' 94 | spec: 95 | containers: 96 | - name: grpc-service 97 | image: test-servers 98 | ports: 99 | - containerPort: 50051 100 | env: 101 | - name: SERVICE_NAME 102 | value: 'grpc-service' 103 | - name: GRPC_SERVICE 104 | value: 'TRUE' 105 | - name: PORT 106 | value: '50051' 107 | imagePullPolicy: Never 108 | 109 | --- 110 | apiVersion: v1 111 | kind: Service 112 | metadata: 113 | name: orders-service 114 | spec: 115 | selector: 116 | app: orders-service 117 | ports: 118 | - protocol: TCP 119 | port: 3000 120 | targetPort: 3000 121 | type: LoadBalancer 122 | 123 | --- 124 | apiVersion: v1 125 | kind: Service 126 | metadata: 127 | name: grpc-service 128 | spec: 129 | selector: 130 | app: grpc-service 131 | ports: 132 | - protocol: TCP 133 | port: 50051 134 | targetPort: 50051 135 | 136 | --- 137 | apiVersion: v1 138 | kind: Service 139 | metadata: 140 | name: emails-service 141 | spec: 142 | selector: 143 | app: emails-service 144 | ports: 145 | - protocol: TCP 146 | port: 3000 147 | targetPort: 3000 148 | 149 | --- 150 | apiVersion: opentelemetry.io/v1alpha1 151 | kind: OpenTelemetryCollector 152 | metadata: 153 | name: otel 154 | spec: 155 | config: | 156 | receivers: 157 | otlp: 158 | protocols: 159 | grpc: 160 | http: 161 | processors: 162 | batch: 163 | timeout: 100ms 164 | 165 | exporters: 166 | logging: 167 | verbosity: detailed 168 | otlphttp: 169 | endpoint: http://host.docker.internal:4123 170 | 171 | service: 172 | pipelines: 173 | traces: 174 | receivers: [otlp] 175 | processors: [batch] 176 | exporters: [logging, otlphttp] 177 | -------------------------------------------------------------------------------- /packages/test-servers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@traceloop/test-servers", 3 | "version": "0.4.0", 4 | "private": true, 5 | "main": "./dist/index.js", 6 | "engines": { 7 | "node": ">=14.0.0" 8 | }, 9 | "scripts": { 10 | "prebuild": "rm -rf dist", 11 | "build": "rollup -c && cp ./src/helloworld.proto ./dist/helloworld.proto", 12 | "start:orders": "ORDERS_SERVICE=TRUE SERVICE_NAME=orders-service PORT=3000 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", 13 | "start:emails": "EMAILS_SERVICE=TRUE SERVICE_NAME=emails-service PORT=3001 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", 14 | "start:grpc": "GRPC_SERVICE=TRUE SERVICE_NAME=grpc-service PORT=50051 OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4123/v1/traces OTEL_EXPORTER_TYPE=PROTO node -r @traceloop/instrument-opentelemetry dist/index.js", 15 | "start": "concurrently \"npm:start:orders\" \"npm:start:emails\" \"npm:start:grpc\"" 16 | }, 17 | "devDependencies": { 18 | "@types/express": "^4.17.17", 19 | "@types/uuid": "^9.0.0", 20 | "rollup": "^3.20.0", 21 | "rollup-plugin-swc3": "^0.8.0" 22 | }, 23 | "dependencies": { 24 | "@grpc/proto-loader": "^0.7.5", 25 | "@traceloop/instrument-opentelemetry": "^0.5.0", 26 | "express": "^4.18.2", 27 | "google-protobuf": "^3.0.0", 28 | "pg": "^8.9.0", 29 | "typeorm": "^0.3.12", 30 | "uuid": "^9.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/test-servers/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import { swc, defineRollupSwcOption } from 'rollup-plugin-swc3'; 2 | import typescript from '@rollup/plugin-typescript'; 3 | 4 | const bundle = (config) => ({ 5 | external: (id) => { 6 | return !/^[./]/.test(id); 7 | }, 8 | ...config, 9 | }); 10 | 11 | const swcPlugin = swc( 12 | defineRollupSwcOption({ 13 | jsc: { target: 'es2021' }, 14 | }), 15 | ); 16 | 17 | export default [ 18 | bundle({ 19 | input: ['src/index.ts'], 20 | output: { 21 | file: 'dist/index.js', 22 | format: 'cjs', 23 | }, 24 | plugins: [swcPlugin, typescript({ tsconfig: '../../tsconfig.json' })], 25 | }), 26 | ]; 27 | -------------------------------------------------------------------------------- /packages/test-servers/src/helloworld.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | option java_multiple_files = true; 4 | option java_package = "io.grpc.examples.helloworld"; 5 | option java_outer_classname = "HelloWorldProto"; 6 | option objc_class_prefix = "HLW"; 7 | 8 | package helloworld; 9 | 10 | // The greeting service definition. 11 | service Greeter { 12 | // Sends a greeting 13 | rpc SayHello (HelloRequest) returns (HelloReply) {} 14 | } 15 | 16 | // The request message containing the user's name. 17 | message HelloRequest { 18 | string name = 1; 19 | } 20 | 21 | // The response message containing the greetings 22 | message HelloReply { 23 | string message = 1; 24 | } 25 | -------------------------------------------------------------------------------- /packages/test-servers/src/index.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import express from 'express'; 3 | import { DataSource } from 'typeorm'; 4 | import { v4 as uuidv4 } from 'uuid'; 5 | import grpc from '@grpc/grpc-js'; 6 | import protoLoader from '@grpc/proto-loader'; 7 | 8 | // --- Protos --- 9 | const PROTO_PATH = __dirname + '/helloworld.proto'; 10 | 11 | const packageDefinition = protoLoader.loadSync(PROTO_PATH, { 12 | keepCase: true, 13 | longs: String, 14 | enums: String, 15 | defaults: true, 16 | oneofs: true, 17 | }); 18 | const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld; 19 | 20 | // --- Postgres --- 21 | const ordersDataSource = new DataSource({ 22 | type: 'postgres', 23 | host: process.env.POSTGRES_HOST || 'localhost', 24 | port: process.env.POSTGRES_PORT ? parseInt(process.env.POSTGRES_PORT) : 5432, 25 | username: process.env.POSTGRES_USERNAME || 'postgres', 26 | password: process.env.POSTGRES_PASSWORD || 'postgres', 27 | database: process.env.POSTGRES_DATABASE || 'postgres', 28 | }); 29 | 30 | const postgresSchema = process.env.POSTGRES_SCHEMA || 'public'; 31 | let ordersDataSourceInitialized = false; 32 | 33 | const initializeOrdersDatasource = async () => 34 | ordersDataSource 35 | .initialize() 36 | .then(async () => { 37 | console.log('Orders data Source has been initialized!'); 38 | await ordersDataSource.query( 39 | `CREATE TABLE IF NOT EXISTS ${postgresSchema}.orders (id varchar(50), price_in_cents int)`, 40 | ); 41 | ordersDataSourceInitialized = true; 42 | console.log('Orders table has been created!'); 43 | }) 44 | .catch((err) => { 45 | console.error('Error during orders data source initialization', err); 46 | }); 47 | 48 | if (process.env.ORDERS_SERVICE) { 49 | initializeOrdersDatasource(); 50 | } 51 | 52 | // --- Orders Service --- 53 | const ordersService = express(); 54 | 55 | ordersService.post('/orders/create', async (req, res) => { 56 | if (!ordersDataSourceInitialized) { 57 | await initializeOrdersDatasource(); 58 | } 59 | 60 | const orderId = uuidv4(); 61 | console.log('Creating order...'); 62 | 63 | try { 64 | ordersDataSource.query( 65 | `INSERT INTO orders (id, price_in_cents) VALUES ('${orderId}', 1000)`, 66 | ); 67 | } catch (err) { 68 | console.error('Error creating order', err); 69 | console.log( 70 | 'Omitting order creation, please make sure the database is running', 71 | ); 72 | } 73 | 74 | // make http call 75 | console.log('Order created! Sending email...'); 76 | const EMAILS_SERVICE_URL = 77 | process.env.EMAILS_SERVICE_URL || 'http://localhost:3001'; 78 | await axios.post(`${EMAILS_SERVICE_URL}/emails/send`, { 79 | email: 'test', 80 | nestedObject: { test: 'test' }, 81 | }); 82 | 83 | // make grpc call 84 | console.log('Making gRPC call'); 85 | const GRPC_SERVICE_URL = process.env.GRPC_SERVICE_URL || 'localhost:50051'; 86 | const client = new (hello_proto as any).Greeter( 87 | GRPC_SERVICE_URL, 88 | grpc.credentials.createInsecure(), 89 | ); 90 | 91 | client.sayHello({ name: 'name' }, function (_: any, response: any) { 92 | console.log('Greeting:', response.message); 93 | }); 94 | 95 | res.send('Order created!'); 96 | }); 97 | 98 | // --- Emails Service --- 99 | const emailsService = express(); 100 | 101 | emailsService.post('/emails/send', (req, res) => { 102 | console.log('Email sent!'); 103 | res.send('Email sent!'); 104 | }); 105 | 106 | // --- gRPC Service --- 107 | const grpcServer = new grpc.Server(); 108 | 109 | function sayHello(call: any, callback: any) { 110 | callback(null, { message: 'Hello ' + call.request.name }); 111 | } 112 | grpcServer.addService((hello_proto as any).Greeter.service, { 113 | sayHello: sayHello, 114 | }); 115 | 116 | // --- Initialize Service --- 117 | const PORT = process.env.PORT || 3000; 118 | 119 | if (process.env.ORDERS_SERVICE) { 120 | ordersService.listen(PORT, () => { 121 | console.log(`Orders service listening at http://localhost:${PORT}`); 122 | }); 123 | } 124 | 125 | if (process.env.EMAILS_SERVICE) { 126 | emailsService.listen(PORT, () => { 127 | console.log(`Emails service listening at http://localhost:${PORT}`); 128 | }); 129 | } 130 | 131 | if (process.env.GRPC_SERVICE) { 132 | grpcServer.bindAsync( 133 | `0.0.0.0:${PORT}`, 134 | grpc.ServerCredentials.createInsecure(), 135 | () => { 136 | console.log(`gRPC service listening on port ${PORT}`); 137 | grpcServer.start(); 138 | }, 139 | ); 140 | } 141 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | "compilerOptions": { 4 | "preserveConstEnums": true, 5 | "allowSyntheticDefaultImports": true, 6 | "allowJs": true, 7 | "declaration": true, 8 | "outDir": "dist" 9 | }, 10 | "include": ["packages/*/src/**/*"], 11 | "exclude": ["node_modules", "dist"] 12 | } 13 | --------------------------------------------------------------------------------