├── .github └── workflows │ └── test.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── diagram.png └── malabilogo.png ├── docs ├── .nojekyll ├── assets │ ├── highlight.css │ ├── icons.css │ ├── icons.png │ ├── icons@2x.png │ ├── main.js │ ├── search.js │ ├── style.css │ ├── widgets.png │ └── widgets@2x.png ├── classes │ ├── TelemetryRepository.html │ └── _internal_.SpansRepository.html ├── index.html ├── modules.html └── modules │ └── _internal_.html ├── examples ├── README.md ├── service-under-test │ ├── package.json │ ├── src │ │ ├── db.ts │ │ ├── index.ts │ │ └── redis.ts │ └── tsconfig.json └── tests-runner │ ├── jest.config.js │ ├── package.json │ ├── test │ ├── service-under-test.spec.ts │ └── tracing.ts │ └── tsconfig.json ├── jsdoc.json ├── lerna.json ├── package.json ├── packages ├── instrumentation-node │ ├── README.md │ ├── package.json │ ├── src │ │ ├── enums.ts │ │ ├── index.ts │ │ ├── instrumentations-config │ │ │ ├── aws-sdk.ts │ │ │ ├── elasticsearch.ts │ │ │ ├── express.ts │ │ │ ├── http.ts │ │ │ ├── index.ts │ │ │ ├── ioredis.ts │ │ │ ├── kafkajs.ts │ │ │ ├── module-version.ts │ │ │ ├── mongoose.ts │ │ │ ├── neo4j.ts │ │ │ └── orm.ts │ │ ├── payload-collection │ │ │ ├── mime-type.ts │ │ │ ├── recording-span.ts │ │ │ └── stream-chunks.ts │ │ └── types.ts │ ├── test │ │ ├── auto-instrumentation.spec.ts │ │ └── instrument.ts │ └── tsconfig.json ├── malabi │ ├── README.md │ ├── package.json │ ├── src │ │ ├── exporter │ │ │ ├── index.ts │ │ │ └── jaeger.ts │ │ ├── index.ts │ │ ├── instrumentation │ │ │ └── index.ts │ │ └── remote-runner-integration │ │ │ ├── fetch-remote-telemetry.ts │ │ │ ├── http-server.ts │ │ │ └── index.ts │ └── tsconfig.json ├── opentelemetry-instrumentation-mocha │ ├── README.md │ ├── multi-reporters-config.json │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── instrumentation.ts │ │ ├── otel-plugin.ts │ │ ├── otel-reporter.js │ │ ├── types.ts │ │ └── version.ts │ ├── test │ │ ├── instrument.js │ │ └── mocha.spec.ts │ └── tsconfig.json ├── opentelemetry-proto-transformations │ ├── package.json │ ├── src │ │ ├── index.ts │ │ ├── opentelemetry │ │ │ └── proto │ │ │ │ ├── collector │ │ │ │ └── trace │ │ │ │ │ └── v1 │ │ │ │ │ ├── trace_service.ts │ │ │ │ │ └── transform.ts │ │ │ │ ├── common │ │ │ │ └── v1 │ │ │ │ │ ├── common.ts │ │ │ │ │ └── transform.ts │ │ │ │ ├── resource │ │ │ │ └── v1 │ │ │ │ │ ├── resource.ts │ │ │ │ │ └── transform.ts │ │ │ │ └── trace │ │ │ │ └── v1 │ │ │ │ ├── trace.ts │ │ │ │ └── transform.ts │ │ └── utils.ts │ └── tsconfig.json ├── telemetry-repository │ ├── package.json │ ├── src │ │ ├── MalabiSpan.ts │ │ ├── SpansRepository.ts │ │ ├── TelemetryRepository.ts │ │ └── index.ts │ ├── test │ │ └── MalabiSpan.spec.ts │ └── tsconfig.json └── tsconfig.base.json └── scripts └── version-update.js /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | branches: [ master ] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/setup-node@v1 12 | with: 13 | node-version: '16' 14 | 15 | - uses: actions/checkout@v2 16 | 17 | - name: Install Dependencies 18 | run: yarn 19 | 20 | - name: Build Packages 21 | run: yarn build 22 | 23 | - name: Install Concurrently & wait-on 24 | run: yarn global add concurrently && yarn global add wait-on 25 | 26 | - name: Run Tests 27 | run: concurrently -k -s first "yarn --cwd examples/service-under-test start:inmemory" "wait-on http://localhost:8080/ && yarn --cwd examples/tests-runner test" 28 | 29 | -------------------------------------------------------------------------------- /.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 | yarn.lock 107 | 108 | .vscode 109 | .idea -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | packages/aspecto-opentelemetry/aspecto-post-install-githash.json 2 | packages/*/dist/* 3 | blackbox/target-service/packages-dist/* 4 | node_modules 5 | *.md 6 | *.yml 7 | lerna.json -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | trailingComma: 'es5' 2 | semi: true 3 | printWidth: 120 4 | tabWidth: 4 5 | singleQuote: true 6 | arrowParens: 'always' 7 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Running locally 2 | ``` 3 | lerna bootstrap 4 | yarn watch 5 | ``` 6 | 7 | # Generating Docs 8 | Run this from root of project: 9 | ``` 10 | typedoc --entryPointStrategy packages 'packages/malabi' 'packages/telemetry-repository/src/SpansRepository.ts' --excludeNotDocumented --excludeInternal 11 | ``` -------------------------------------------------------------------------------- /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 | Malabi 3 |

4 |

5 | OpenTelemetry based Javascript test framework 6 |

7 | 8 | 9 | Malabi is released under the Apache-2.0 license. 10 | 11 | 12 | # Description 13 | This library introduces a new way of testing services: **Trace-based testing** (TBT). It is very useful when you want to validate integration between different parts. For example: make sure elasticsearch received the correct params on insert. 14 | 15 | - 💻 **Developer friendly**: Built by developers, for developers who love distributed applications. 16 | 17 | - ✅ **Validate integration**: Access to validate any backend interaction, fast, simple and reliable. 18 | 19 | - 🔗 **OpenTelemetry based**: Built based on OpenTelemetry to match the characteristics of distributed apps. 20 | 21 | 22 | ## How it works 23 | How it work diagram 24 | 25 | There are two main components to Malabi: 26 | 27 | 1. An OpenTelemetry SDK Distribution - used to collect any activity in the service under test by instrumenting it. **It is stored in the memory of the asserted service or in a Jaeger instance **, and exposes and endpoint for the test runner to access & make assertions. 28 | 29 | 2. An assertion library for OpenTelemetry data - by using the `malabi` wrapper function you will get access to any span created by the current test, then you will be able to validate the span and the service behavior 30 | 31 | ## Getting started 32 | ### In the microservice you want to test 33 | 1. ```npm install --save malabi``` or ```yarn add malabi``` 34 | 2. Add the following code at the service initialization, for example: in index.js. needs to be before any other imports to work properly. 35 | 36 | ```JS 37 | import { instrument, serveMalabiFromHttpApp } from 'malabi'; 38 | const instrumentationConfig = { 39 | serviceName: 'service-under-test', 40 | }; 41 | instrument(instrumentationConfig); 42 | serveMalabiFromHttpApp(18393, instrumentationConfig); 43 | 44 | import axios from 'axios'; 45 | import express from 'express'; 46 | import User from "./db"; 47 | const PORT = process.env.PORT || 8080; 48 | 49 | const app = express(); 50 | 51 | app.get('/todo', async (req, res) => { 52 | try { 53 | const todoItem = await axios('https://jsonplaceholder.typicode.com/todos/1'); 54 | res.json({ 55 | title: todoItem.data.title, 56 | }); 57 | } catch (e) { 58 | res.sendStatus(500); 59 | console.error(e, e); 60 | } 61 | }); 62 | ``` 63 | 64 | ## In your test file 65 | Create a tracing.ts file to set up instrumentation on the tests runner(this enables us to separate spans created in one test from other tests' spans from the other): 66 | ```JS 67 | import { instrument } from 'malabi'; 68 | 69 | instrument({ 70 | serviceName: 'tests-runner', 71 | }); 72 | ``` 73 | 74 | And this is how the test file looks like(service-under-test.spec.ts): 75 | Note: this should be run with node --require. 76 | 77 | Also, you must provide the MALABI_ENDPOINT_PORT_OR_URL env var (must start with http for url) 78 | ``` 79 | MALABI_ENDPOINT_PORT_OR_URL=http://localhost:18393 ts-mocha --paths "./test/*.ts" --require "./test/tracing.ts" 80 | ``` 81 | Or alternatively just with port(assuming localhost by default): 82 | ``` 83 | MALABI_ENDPOINT_PORT_OR_URL=18393 ts-mocha --paths "./test/*.ts" --require "./test/tracing.ts" 84 | ``` 85 | 86 | Sample test code: 87 | ```JS 88 | const SERVICE_UNDER_TEST_PORT = process.env.PORT || 8080; 89 | import { malabi } from 'malabi'; 90 | 91 | import { expect } from 'chai'; 92 | import axios from 'axios'; 93 | 94 | describe('testing service-under-test remotely', () => { 95 | it('successful /todo request', async () => { 96 | // get spans created from the previous call 97 | const telemetryRepo = await malabi(async () => { 98 | await axios(`http://localhost:${SERVICE_UNDER_TEST_PORT}/todo`); 99 | }); 100 | 101 | // Validate internal HTTP call 102 | const todoInternalHTTPCall = telemetryRepo.spans.outgoing().first; 103 | expect(todoInternalHTTPCall.httpFullUrl).equals('https://jsonplaceholder.typicode.com/todos/1') 104 | expect(todoInternalHTTPCall.statusCode).equals(200); 105 | }); 106 | }); 107 | ``` 108 | 109 | Notice the usage of the malabi function - any piece of code that we put inside the callback given to this function would be instrumented as part 110 | of a newly created trace (created by malabi), and the return value would be the telemetry repository for this test, meaning the 111 | Open Telemetry data you can make assertions on (the spans that were created because of the code you put in the callback). 112 | 113 | To sum it up, be sure that whenever you want to make assertions on a span - the code that created it must be in the callback the malabi function receives, and the malabi function returns the spans created. 114 | 115 | ## Storage Backends 116 | Malabi supports 2 types of storage backends for the telemetry data created in your test (spans and traces). 117 | 1. InMemory - In this mode malabi stores the data in memory. 118 | To select this mode, set MALABI_STORAGE_BACKEND env var to `InMemory` 119 | 2. Jaeger - To select this mode, set MALABI_STORAGE_BACKEND env var to `Jaeger` when running your service under test. 120 | 121 | Also, you can control additional env vars here: 122 | 1. OTEL_EXPORTER_JAEGER_AGENT_HOST - lets you control the hostname of the jaeger agent. it must be running somewhere for this mode to work and it's up to you to make it run. default: `localhost` 123 | Example values: `localhost`,`example.com`. 124 | 2. OTEL_EXPORTER_JAEGER_AGENT_PORT - port of jaeger agent. default: `6832` 125 | 3. MALABI_JAEGER_QUERY_PROTOCOL - the protocol used to query jaeger API for the spans. Either `http`(default) or `https`. 126 | 4. MALABI_JAEGER_QUERY_PORT - the port which we use to query jaeger. default: `16686` 127 | 5. MALABI_JAEGER_QUERY_HOST - ets you control the hostname of the jaeger query api. default: `localhost` 128 | 129 | For both storage backends, malabi creates an endpoint (hosted inside the service-under-test) for the test runner to call query. 130 | 131 | ## Caveat: Usage with Jest 132 | 133 | Currently, Jest does not play out well with OpenTelemetry due to Jest's modifications of the way modules are required and OTEL's usage of 134 | require in the middle. 135 | 136 | Until this is fixed, we recommend using Malabi with Mocha instead of Jest. 137 | 138 | ## Documentation 139 | [Click to view documentation](https://aspecto-io.github.io/malabi/index.html) 140 | 141 | ## Why should you care about Malabi 142 | Most distributed apps developers choose to have some kind of black box test (API, integration, end to end, UI, you name it!). 143 | 144 | Black box test create real network activity which is instrumented by OpenTelemetry (which you should have regardless of Malabi). 145 | 146 | Imagine that you can take any existing black box test and validate any backend activity created by it. 147 | 148 | #### Common use case 149 | You are running an API call that create a new DB record, then you write dedicated test code to fetch the record created and validate it. 150 | Now you can rely on Malabi to validate that mongo got the right data with no special code. 151 | 152 | ## Trace based testing explained 153 | Trace-based testing is a method that allows us to improve assertion capabilities by leveraging traces data and make it accessible while setting our expectations from a test. That enables us to **validate essential relationships between software components that otherwise are put to the test only in production**. 154 | Trace-based validation enables developers to become proactive to issues instead of reactive. 155 | ## More examples 156 | 157 | ```JS 158 | import { malabi } from 'malabi'; 159 | 160 | it('should select from db', async () => { 161 | const { spans } = await malabi(async () => { 162 | // some code here that makes db operations with sequelize 163 | }); 164 | 165 | // Validating that /users had ran a single select statement and responded with an array. 166 | const sequelizeActivities = spans.sequelize(); 167 | expect(sequelizeActivities.length).toBe(1); 168 | expect(sequelizeActivities.first.dbOperation).toBe("SELECT"); 169 | expect(Array.isArray(JSON.parse(sequelizeActivities.first.dbResponse))).toBe(true); 170 | }); 171 | ``` 172 | 173 | [See in-repo live example](https://github.com/aspecto-io/malabi/tree/master/examples/README.md) 174 | 175 | ## Project Status 176 | Malabi project is actively maintained by [Aspecto](https://www.aspecto.io), and is currently in it's initial days. We would love to receive your feedback, ideas & contributions in the [discussions](https://github.com/aspecto-io/malabi/discussions) section. 177 | -------------------------------------------------------------------------------- /assets/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspecto-io/malabi/fdcff31dd186ebdbeb3a5d1b1d11f63dc5ae8a5a/assets/diagram.png -------------------------------------------------------------------------------- /assets/malabilogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspecto-io/malabi/fdcff31dd186ebdbeb3a5d1b1d11f63dc5ae8a5a/assets/malabilogo.png -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #AF00DB; 3 | --dark-hl-0: #C586C0; 4 | --light-hl-1: #000000; 5 | --dark-hl-1: #D4D4D4; 6 | --light-hl-2: #001080; 7 | --dark-hl-2: #9CDCFE; 8 | --light-hl-3: #A31515; 9 | --dark-hl-3: #CE9178; 10 | --light-hl-4: #0000FF; 11 | --dark-hl-4: #569CD6; 12 | --light-hl-5: #0070C1; 13 | --dark-hl-5: #4FC1FF; 14 | --light-hl-6: #795E26; 15 | --dark-hl-6: #DCDCAA; 16 | --light-hl-7: #098658; 17 | --dark-hl-7: #B5CEA8; 18 | --light-hl-8: #000000; 19 | --dark-hl-8: #C8C8C8; 20 | --light-hl-9: #008000; 21 | --dark-hl-9: #6A9955; 22 | --light-hl-10: #267F99; 23 | --dark-hl-10: #4EC9B0; 24 | --light-code-background: #F5F5F5; 25 | --dark-code-background: #1E1E1E; 26 | } 27 | 28 | @media (prefers-color-scheme: light) { :root { 29 | --hl-0: var(--light-hl-0); 30 | --hl-1: var(--light-hl-1); 31 | --hl-2: var(--light-hl-2); 32 | --hl-3: var(--light-hl-3); 33 | --hl-4: var(--light-hl-4); 34 | --hl-5: var(--light-hl-5); 35 | --hl-6: var(--light-hl-6); 36 | --hl-7: var(--light-hl-7); 37 | --hl-8: var(--light-hl-8); 38 | --hl-9: var(--light-hl-9); 39 | --hl-10: var(--light-hl-10); 40 | --code-background: var(--light-code-background); 41 | } } 42 | 43 | @media (prefers-color-scheme: dark) { :root { 44 | --hl-0: var(--dark-hl-0); 45 | --hl-1: var(--dark-hl-1); 46 | --hl-2: var(--dark-hl-2); 47 | --hl-3: var(--dark-hl-3); 48 | --hl-4: var(--dark-hl-4); 49 | --hl-5: var(--dark-hl-5); 50 | --hl-6: var(--dark-hl-6); 51 | --hl-7: var(--dark-hl-7); 52 | --hl-8: var(--dark-hl-8); 53 | --hl-9: var(--dark-hl-9); 54 | --hl-10: var(--dark-hl-10); 55 | --code-background: var(--dark-code-background); 56 | } } 57 | 58 | body.light { 59 | --hl-0: var(--light-hl-0); 60 | --hl-1: var(--light-hl-1); 61 | --hl-2: var(--light-hl-2); 62 | --hl-3: var(--light-hl-3); 63 | --hl-4: var(--light-hl-4); 64 | --hl-5: var(--light-hl-5); 65 | --hl-6: var(--light-hl-6); 66 | --hl-7: var(--light-hl-7); 67 | --hl-8: var(--light-hl-8); 68 | --hl-9: var(--light-hl-9); 69 | --hl-10: var(--light-hl-10); 70 | --code-background: var(--light-code-background); 71 | } 72 | 73 | body.dark { 74 | --hl-0: var(--dark-hl-0); 75 | --hl-1: var(--dark-hl-1); 76 | --hl-2: var(--dark-hl-2); 77 | --hl-3: var(--dark-hl-3); 78 | --hl-4: var(--dark-hl-4); 79 | --hl-5: var(--dark-hl-5); 80 | --hl-6: var(--dark-hl-6); 81 | --hl-7: var(--dark-hl-7); 82 | --hl-8: var(--dark-hl-8); 83 | --hl-9: var(--dark-hl-9); 84 | --hl-10: var(--dark-hl-10); 85 | --code-background: var(--dark-code-background); 86 | } 87 | 88 | .hl-0 { color: var(--hl-0); } 89 | .hl-1 { color: var(--hl-1); } 90 | .hl-2 { color: var(--hl-2); } 91 | .hl-3 { color: var(--hl-3); } 92 | .hl-4 { color: var(--hl-4); } 93 | .hl-5 { color: var(--hl-5); } 94 | .hl-6 { color: var(--hl-6); } 95 | .hl-7 { color: var(--hl-7); } 96 | .hl-8 { color: var(--hl-8); } 97 | .hl-9 { color: var(--hl-9); } 98 | .hl-10 { color: var(--hl-10); } 99 | pre, code { background: var(--code-background); } 100 | -------------------------------------------------------------------------------- /docs/assets/icons.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspecto-io/malabi/fdcff31dd186ebdbeb3a5d1b1d11f63dc5ae8a5a/docs/assets/icons.png -------------------------------------------------------------------------------- /docs/assets/icons@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspecto-io/malabi/fdcff31dd186ebdbeb3a5d1b1d11f63dc5ae8a5a/docs/assets/icons@2x.png -------------------------------------------------------------------------------- /docs/assets/search.js: -------------------------------------------------------------------------------- 1 | window.searchData = JSON.parse("{\"kinds\":{\"4\":\"Namespace\",\"64\":\"Function\",\"128\":\"Class\",\"512\":\"Constructor\",\"1024\":\"Property\",\"2048\":\"Method\",\"262144\":\"Accessor\"},\"rows\":[{\"id\":0,\"kind\":64,\"name\":\"instrument\",\"url\":\"modules.html#instrument\",\"classes\":\"tsd-kind-function\"},{\"id\":1,\"kind\":64,\"name\":\"malabi\",\"url\":\"modules.html#malabi\",\"classes\":\"tsd-kind-function\"},{\"id\":2,\"kind\":64,\"name\":\"serveMalabiFromHttpApp\",\"url\":\"modules.html#serveMalabiFromHttpApp\",\"classes\":\"tsd-kind-function\"},{\"id\":3,\"kind\":128,\"name\":\"TelemetryRepository\",\"url\":\"classes/TelemetryRepository.html\",\"classes\":\"tsd-kind-class\"},{\"id\":4,\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/TelemetryRepository.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\"TelemetryRepository\"},{\"id\":5,\"kind\":262144,\"name\":\"spans\",\"url\":\"classes/TelemetryRepository.html#spans\",\"classes\":\"tsd-kind-get-signature tsd-parent-kind-class\",\"parent\":\"TelemetryRepository\"},{\"id\":6,\"kind\":4,\"name\":\"\",\"url\":\"modules/_internal_.html\",\"classes\":\"tsd-kind-namespace\"},{\"id\":7,\"kind\":128,\"name\":\"SpansRepository\",\"url\":\"classes/_internal_.SpansRepository.html\",\"classes\":\"tsd-kind-class tsd-parent-kind-namespace\",\"parent\":\"\"},{\"id\":8,\"kind\":512,\"name\":\"constructor\",\"url\":\"classes/_internal_.SpansRepository.html#constructor\",\"classes\":\"tsd-kind-constructor tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":9,\"kind\":1024,\"name\":\"filter\",\"url\":\"classes/_internal_.SpansRepository.html#filter\",\"classes\":\"tsd-kind-property tsd-parent-kind-class tsd-is-private\",\"parent\":\".SpansRepository\"},{\"id\":10,\"kind\":262144,\"name\":\"length\",\"url\":\"classes/_internal_.SpansRepository.html#length\",\"classes\":\"tsd-kind-get-signature tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":11,\"kind\":262144,\"name\":\"first\",\"url\":\"classes/_internal_.SpansRepository.html#first\",\"classes\":\"tsd-kind-get-signature tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":12,\"kind\":262144,\"name\":\"second\",\"url\":\"classes/_internal_.SpansRepository.html#second\",\"classes\":\"tsd-kind-get-signature tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":13,\"kind\":262144,\"name\":\"all\",\"url\":\"classes/_internal_.SpansRepository.html#all\",\"classes\":\"tsd-kind-get-signature tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":14,\"kind\":2048,\"name\":\"at\",\"url\":\"classes/_internal_.SpansRepository.html#at\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":15,\"kind\":2048,\"name\":\"http\",\"url\":\"classes/_internal_.SpansRepository.html#http\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":16,\"kind\":2048,\"name\":\"httpMethod\",\"url\":\"classes/_internal_.SpansRepository.html#httpMethod\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":17,\"kind\":2048,\"name\":\"httpGet\",\"url\":\"classes/_internal_.SpansRepository.html#httpGet\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":18,\"kind\":2048,\"name\":\"httpPost\",\"url\":\"classes/_internal_.SpansRepository.html#httpPost\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":19,\"kind\":2048,\"name\":\"route\",\"url\":\"classes/_internal_.SpansRepository.html#route\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":20,\"kind\":2048,\"name\":\"path\",\"url\":\"classes/_internal_.SpansRepository.html#path\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":21,\"kind\":2048,\"name\":\"messagingSend\",\"url\":\"classes/_internal_.SpansRepository.html#messagingSend\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":22,\"kind\":2048,\"name\":\"messagingReceive\",\"url\":\"classes/_internal_.SpansRepository.html#messagingReceive\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":23,\"kind\":2048,\"name\":\"messagingProcess\",\"url\":\"classes/_internal_.SpansRepository.html#messagingProcess\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":24,\"kind\":2048,\"name\":\"awsSqs\",\"url\":\"classes/_internal_.SpansRepository.html#awsSqs\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":25,\"kind\":2048,\"name\":\"entry\",\"url\":\"classes/_internal_.SpansRepository.html#entry\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":26,\"kind\":2048,\"name\":\"mongo\",\"url\":\"classes/_internal_.SpansRepository.html#mongo\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":27,\"kind\":2048,\"name\":\"incoming\",\"url\":\"classes/_internal_.SpansRepository.html#incoming\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":28,\"kind\":2048,\"name\":\"outgoing\",\"url\":\"classes/_internal_.SpansRepository.html#outgoing\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":29,\"kind\":2048,\"name\":\"express\",\"url\":\"classes/_internal_.SpansRepository.html#express\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":30,\"kind\":2048,\"name\":\"typeorm\",\"url\":\"classes/_internal_.SpansRepository.html#typeorm\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":31,\"kind\":2048,\"name\":\"sequelize\",\"url\":\"classes/_internal_.SpansRepository.html#sequelize\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":32,\"kind\":2048,\"name\":\"neo4j\",\"url\":\"classes/_internal_.SpansRepository.html#neo4j\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":33,\"kind\":2048,\"name\":\"database\",\"url\":\"classes/_internal_.SpansRepository.html#database\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":34,\"kind\":2048,\"name\":\"dbOperation\",\"url\":\"classes/_internal_.SpansRepository.html#dbOperation\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":35,\"kind\":2048,\"name\":\"messaging\",\"url\":\"classes/_internal_.SpansRepository.html#messaging\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":36,\"kind\":2048,\"name\":\"rpc\",\"url\":\"classes/_internal_.SpansRepository.html#rpc\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":37,\"kind\":2048,\"name\":\"redis\",\"url\":\"classes/_internal_.SpansRepository.html#redis\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"},{\"id\":38,\"kind\":2048,\"name\":\"aws\",\"url\":\"classes/_internal_.SpansRepository.html#aws\",\"classes\":\"tsd-kind-method tsd-parent-kind-class\",\"parent\":\".SpansRepository\"}],\"index\":{\"version\":\"2.3.9\",\"fields\":[\"name\",\"parent\"],\"fieldVectors\":[[\"name/0\",[0,32.834]],[\"parent/0\",[]],[\"name/1\",[1,32.834]],[\"parent/1\",[]],[\"name/2\",[2,32.834]],[\"parent/2\",[]],[\"name/3\",[3,24.361]],[\"parent/3\",[]],[\"name/4\",[4,27.726]],[\"parent/4\",[3,2.298]],[\"name/5\",[5,32.834]],[\"parent/5\",[3,2.298]],[\"name/6\",[6,27.726]],[\"parent/6\",[]],[\"name/7\",[7,32.834]],[\"parent/7\",[6,2.615]],[\"name/8\",[4,27.726]],[\"parent/8\",[8,0.225]],[\"name/9\",[9,32.834]],[\"parent/9\",[8,0.225]],[\"name/10\",[10,32.834]],[\"parent/10\",[8,0.225]],[\"name/11\",[11,32.834]],[\"parent/11\",[8,0.225]],[\"name/12\",[12,32.834]],[\"parent/12\",[8,0.225]],[\"name/13\",[13,32.834]],[\"parent/13\",[8,0.225]],[\"name/14\",[14,32.834]],[\"parent/14\",[8,0.225]],[\"name/15\",[15,32.834]],[\"parent/15\",[8,0.225]],[\"name/16\",[16,32.834]],[\"parent/16\",[8,0.225]],[\"name/17\",[17,32.834]],[\"parent/17\",[8,0.225]],[\"name/18\",[18,32.834]],[\"parent/18\",[8,0.225]],[\"name/19\",[19,32.834]],[\"parent/19\",[8,0.225]],[\"name/20\",[20,32.834]],[\"parent/20\",[8,0.225]],[\"name/21\",[21,32.834]],[\"parent/21\",[8,0.225]],[\"name/22\",[22,32.834]],[\"parent/22\",[8,0.225]],[\"name/23\",[23,32.834]],[\"parent/23\",[8,0.225]],[\"name/24\",[24,32.834]],[\"parent/24\",[8,0.225]],[\"name/25\",[25,32.834]],[\"parent/25\",[8,0.225]],[\"name/26\",[26,32.834]],[\"parent/26\",[8,0.225]],[\"name/27\",[27,32.834]],[\"parent/27\",[8,0.225]],[\"name/28\",[28,32.834]],[\"parent/28\",[8,0.225]],[\"name/29\",[29,32.834]],[\"parent/29\",[8,0.225]],[\"name/30\",[30,32.834]],[\"parent/30\",[8,0.225]],[\"name/31\",[31,32.834]],[\"parent/31\",[8,0.225]],[\"name/32\",[32,32.834]],[\"parent/32\",[8,0.225]],[\"name/33\",[33,32.834]],[\"parent/33\",[8,0.225]],[\"name/34\",[34,32.834]],[\"parent/34\",[8,0.225]],[\"name/35\",[35,32.834]],[\"parent/35\",[8,0.225]],[\"name/36\",[36,32.834]],[\"parent/36\",[8,0.225]],[\"name/37\",[37,32.834]],[\"parent/37\",[8,0.225]],[\"name/38\",[38,32.834]],[\"parent/38\",[8,0.225]]],\"invertedIndex\":[[\"all\",{\"_index\":13,\"name\":{\"13\":{}},\"parent\":{}}],[\"at\",{\"_index\":14,\"name\":{\"14\":{}},\"parent\":{}}],[\"aws\",{\"_index\":38,\"name\":{\"38\":{}},\"parent\":{}}],[\"awssqs\",{\"_index\":24,\"name\":{\"24\":{}},\"parent\":{}}],[\"constructor\",{\"_index\":4,\"name\":{\"4\":{},\"8\":{}},\"parent\":{}}],[\"database\",{\"_index\":33,\"name\":{\"33\":{}},\"parent\":{}}],[\"dboperation\",{\"_index\":34,\"name\":{\"34\":{}},\"parent\":{}}],[\"entry\",{\"_index\":25,\"name\":{\"25\":{}},\"parent\":{}}],[\"express\",{\"_index\":29,\"name\":{\"29\":{}},\"parent\":{}}],[\"filter\",{\"_index\":9,\"name\":{\"9\":{}},\"parent\":{}}],[\"first\",{\"_index\":11,\"name\":{\"11\":{}},\"parent\":{}}],[\"http\",{\"_index\":15,\"name\":{\"15\":{}},\"parent\":{}}],[\"httpget\",{\"_index\":17,\"name\":{\"17\":{}},\"parent\":{}}],[\"httpmethod\",{\"_index\":16,\"name\":{\"16\":{}},\"parent\":{}}],[\"httppost\",{\"_index\":18,\"name\":{\"18\":{}},\"parent\":{}}],[\"incoming\",{\"_index\":27,\"name\":{\"27\":{}},\"parent\":{}}],[\"instrument\",{\"_index\":0,\"name\":{\"0\":{}},\"parent\":{}}],[\"internal\",{\"_index\":6,\"name\":{\"6\":{}},\"parent\":{\"7\":{}}}],[\"internal>.spansrepository\",{\"_index\":8,\"name\":{},\"parent\":{\"8\":{},\"9\":{},\"10\":{},\"11\":{},\"12\":{},\"13\":{},\"14\":{},\"15\":{},\"16\":{},\"17\":{},\"18\":{},\"19\":{},\"20\":{},\"21\":{},\"22\":{},\"23\":{},\"24\":{},\"25\":{},\"26\":{},\"27\":{},\"28\":{},\"29\":{},\"30\":{},\"31\":{},\"32\":{},\"33\":{},\"34\":{},\"35\":{},\"36\":{},\"37\":{},\"38\":{}}}],[\"length\",{\"_index\":10,\"name\":{\"10\":{}},\"parent\":{}}],[\"malabi\",{\"_index\":1,\"name\":{\"1\":{}},\"parent\":{}}],[\"messaging\",{\"_index\":35,\"name\":{\"35\":{}},\"parent\":{}}],[\"messagingprocess\",{\"_index\":23,\"name\":{\"23\":{}},\"parent\":{}}],[\"messagingreceive\",{\"_index\":22,\"name\":{\"22\":{}},\"parent\":{}}],[\"messagingsend\",{\"_index\":21,\"name\":{\"21\":{}},\"parent\":{}}],[\"mongo\",{\"_index\":26,\"name\":{\"26\":{}},\"parent\":{}}],[\"neo4j\",{\"_index\":32,\"name\":{\"32\":{}},\"parent\":{}}],[\"outgoing\",{\"_index\":28,\"name\":{\"28\":{}},\"parent\":{}}],[\"path\",{\"_index\":20,\"name\":{\"20\":{}},\"parent\":{}}],[\"redis\",{\"_index\":37,\"name\":{\"37\":{}},\"parent\":{}}],[\"route\",{\"_index\":19,\"name\":{\"19\":{}},\"parent\":{}}],[\"rpc\",{\"_index\":36,\"name\":{\"36\":{}},\"parent\":{}}],[\"second\",{\"_index\":12,\"name\":{\"12\":{}},\"parent\":{}}],[\"sequelize\",{\"_index\":31,\"name\":{\"31\":{}},\"parent\":{}}],[\"servemalabifromhttpapp\",{\"_index\":2,\"name\":{\"2\":{}},\"parent\":{}}],[\"spans\",{\"_index\":5,\"name\":{\"5\":{}},\"parent\":{}}],[\"spansrepository\",{\"_index\":7,\"name\":{\"7\":{}},\"parent\":{}}],[\"telemetryrepository\",{\"_index\":3,\"name\":{\"3\":{}},\"parent\":{\"4\":{},\"5\":{}}}],[\"typeorm\",{\"_index\":30,\"name\":{\"30\":{}},\"parent\":{}}]],\"pipeline\":[]}}"); -------------------------------------------------------------------------------- /docs/assets/widgets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspecto-io/malabi/fdcff31dd186ebdbeb3a5d1b1d11f63dc5ae8a5a/docs/assets/widgets.png -------------------------------------------------------------------------------- /docs/assets/widgets@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspecto-io/malabi/fdcff31dd186ebdbeb3a5d1b1d11f63dc5ae8a5a/docs/assets/widgets@2x.png -------------------------------------------------------------------------------- /docs/classes/TelemetryRepository.html: -------------------------------------------------------------------------------- 1 | TelemetryRepository | malabi
Options
All
  • Public
  • Public/Protected
  • All
Menu

Class TelemetryRepository

2 |

A Class that allows access of telemetry from the test run. for example: HTTP GET spans. Mongo db spans, etc.

3 |

Read more about OpenTelemetry here

4 |

Hierarchy

  • TelemetryRepository

Index

Constructors

Accessors

Constructors

  • 5 |

    Fetches the spans from the exposed malabi spans endpoint

    6 |

    Parameters

    • spans: ReadableSpan[]
      7 |

      An array of ReadableSpans

      8 |

    Returns TelemetryRepository

Accessors

  • 9 |

    Get the SpansRepository object that allows you to do filtering on the spans. chaining is filters supported.

    10 |

    Returns SpansRepository

Legend

  • Constructor

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/modules.html: -------------------------------------------------------------------------------- 1 | malabi
Options
All
  • Public
  • Public/Protected
  • All
Menu

malabi

Index

Namespaces

Classes

Functions Main Functions

Functions Other

Main Functions Functions

  • instrument(InstrumentationConfig: InstrumentationConfig): void
  • 2 |

    Enables OpenTelemetry instrumentation for Malabi. Used in both test runner and service under test

    3 |

    Parameters

    • InstrumentationConfig: InstrumentationConfig
      4 |

      Config for creating the instrumentation

      5 |

    Returns void

  • 6 |

    A wrapper that handles creating a span per test run. returns the spans that were created inside the callback function ready for assertion.

    7 |

    Parameters

    • callback: any
      8 |

      an async function containing all of the current test's span generating operations(API calls etc)

      9 |

    Returns Promise<TelemetryRepository>

Other Functions

  • serveMalabiFromHttpApp(port: number, instrumentationConfig: InstrumentationConfig): any
  • 10 |

    Exposes an endpoint that returns spans created during the test run

    11 |

    Parameters

    • port: number
      12 |

      the port on which to expose the malabi endpoint

      13 |
    • instrumentationConfig: InstrumentationConfig
      14 |

      contains the service name being tested. example: { serviceName: 'some-service' }

      15 |

    Returns any

Legend

  • Constructor

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /docs/modules/_internal_.html: -------------------------------------------------------------------------------- 1 | <internal> | malabi
Options
All
  • Public
  • Public/Protected
  • All
Menu

Namespace <internal>

Legend

  • Constructor

Settings

Theme

Generated using TypeDoc

-------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## What do we have here 2 | 1. A microservice called `service-under-test` with `malabi` installed 3 | 2. A test runner validating the microservices (called `tests-runner`) 4 | 5 | ## Take it for a test ride 6 | You can find an example service and test to show case how it works. 7 | 8 | 1. In the **project root** run `yarn` to install dependencies, followed by `yarn build`. 9 | 10 | 2. Start the **service-under-test** by running: 11 | ```sh 12 | yarn --cwd examples/service-under-test start 13 | ``` 14 | 3. In a different terminal process, run the tests: 15 | ```sh 16 | yarn --cwd examples/tests-runner test:example 17 | ``` -------------------------------------------------------------------------------- /examples/service-under-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "service-under-test", 3 | "version": "0.0.7-alpha.1", 4 | "description": "example for service being tested with open telemetry instrumentation", 5 | "repository": "https://github.com/aspecto-io/malabi", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "scripts": { 9 | "start": "ts-node src/index.ts -r malabi/instrument", 10 | "start:inmemory": "MALABI_STORAGE_BACKEND=InMemory yarn start", 11 | "start:jaeger": "MALABI_STORAGE_BACKEND=Jaeger OTEL_EXPORTER_JAEGER_AGENT_HOST=\"localhost\" MALABI_JAEGER_QUERY_HOST=\"localhost\" OTEL_EXPORTER_JAEGER_AGENT_PORT=6832 MALABI_JAEGER_QUERY_PORT=16686 MALABI_JAEGER_QUERY_PROTOCOL=http yarn start", 12 | "start:dev": "ts-node-dev src/index.ts", 13 | "build": "tsc" 14 | }, 15 | "dependencies": { 16 | "@types/ioredis": "^4.26.7", 17 | "axios": "^0.21.1", 18 | "body-parser": "^1.19.0", 19 | "express": "^4.17.1", 20 | "ioredis": "^4.27.8", 21 | "redis-memory-server": "^0.10.0", 22 | "sequelize": "^6.6.5", 23 | "sqlite3": "^5.0.2", 24 | "ts-node": "^10.9.2", 25 | "typescript": "^4.2.4" 26 | }, 27 | "devDependencies": { 28 | "malabi": "^0.0.7-alpha.1", 29 | "ts-node-dev": "^1.1.6" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/service-under-test/src/db.ts: -------------------------------------------------------------------------------- 1 | import { Sequelize, DataTypes } from 'sequelize'; 2 | 3 | const sequelize = new Sequelize({ 4 | dialect: 'sqlite', 5 | storage: ':memory:' 6 | }); 7 | 8 | const User = sequelize.define('User', { 9 | firstName: { 10 | type: DataTypes.STRING, 11 | allowNull: false 12 | }, 13 | lastName: { 14 | type: DataTypes.STRING 15 | } 16 | }); 17 | 18 | User.sync({ force: true }).then(() => { 19 | User.create({ firstName: "Rick", lastName: 'Sanchez' }); 20 | }) 21 | 22 | export default User; -------------------------------------------------------------------------------- /examples/service-under-test/src/index.ts: -------------------------------------------------------------------------------- 1 | import { instrument, serveMalabiFromHttpApp } from 'malabi'; 2 | const instrumentationConfig = { 3 | serviceName: 'service-under-test', 4 | }; 5 | instrument(instrumentationConfig); 6 | serveMalabiFromHttpApp(18393, instrumentationConfig); 7 | 8 | import axios from 'axios'; 9 | import express from 'express'; 10 | import body from "body-parser"; 11 | import User from "./db"; 12 | import { getRedis } from "./redis"; 13 | import Redis from "ioredis"; 14 | let redis: Redis.Redis; 15 | 16 | getRedis().then((redisConn) => { 17 | redis = redisConn; 18 | app.listen(PORT, () => console.log(`service-under-test started at port ${PORT}`)); 19 | }) 20 | const PORT = process.env.PORT || 8080; 21 | 22 | const app = express(); 23 | app.use(body.json()) 24 | app.get('/',(req,res)=>{ 25 | res.sendStatus(200); 26 | }) 27 | app.get('/todo', async (req, res) => { 28 | try { 29 | const todoItem = await axios('https://jsonplaceholder.typicode.com/todos/1'); 30 | res.json({ 31 | title: todoItem.data.title, 32 | }); 33 | } catch (e) { 34 | res.sendStatus(500); 35 | console.error(e, e); 36 | } 37 | }); 38 | 39 | app.get('/users', async (req, res) => { 40 | try { 41 | const users = await User.findAll({}); 42 | res.json(users); 43 | } catch (e) { 44 | res.sendStatus(500); 45 | console.error(e, e); 46 | } 47 | }); 48 | 49 | app.get('/users/:firstName', async (req, res) => { 50 | try { 51 | const firstName = req.param('firstName'); 52 | if (!firstName) { 53 | res.status(400).json({ message: 'Missing firstName in url' }); 54 | return; 55 | } 56 | 57 | let users = []; 58 | users = await redis.lrange(firstName, 0, -1); 59 | if (users.length === 0) { 60 | users = await User.findAll({ where: { firstName } }); 61 | if (users.length !== 0) { 62 | await redis.lpush(firstName, users) 63 | } 64 | } 65 | 66 | res.json(users); 67 | } catch (e) { 68 | res.sendStatus(500); 69 | console.error(e, e); 70 | } 71 | }); 72 | 73 | app.post('/users', async (req, res) => { 74 | try { 75 | const { firstName, lastName } = req.body; 76 | const user = await User.create({ firstName, lastName }); 77 | res.json(user); 78 | } catch (e) { 79 | res.sendStatus(500); 80 | } 81 | }) 82 | 83 | 84 | -------------------------------------------------------------------------------- /examples/service-under-test/src/redis.ts: -------------------------------------------------------------------------------- 1 | import { RedisMemoryServer } from 'redis-memory-server'; 2 | import Redis from "ioredis"; 3 | const redisServer = new RedisMemoryServer(); 4 | 5 | export async function getRedis() { 6 | const host = await redisServer.getHost(); 7 | const port = await redisServer.getPort(); 8 | const redis = new Redis(port, host); 9 | return redis; 10 | } -------------------------------------------------------------------------------- /examples/service-under-test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "outDir": "./dist", 5 | "lib": ["es2019"], 6 | "target": "ES5", 7 | "esModuleInterop": true, 8 | "skipLibCheck": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /examples/tests-runner/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; 5 | -------------------------------------------------------------------------------- /examples/tests-runner/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tests-runner", 3 | "version": "0.0.7-alpha.1", 4 | "description": "run remote tests on 'service-under-test'", 5 | "repository": "https://github.com/aspecto-io/malabi", 6 | "license": "Apache-2.0", 7 | "private": true, 8 | "scripts": { 9 | "test": "MALABI_ENDPOINT_PORT_OR_URL=http://localhost:18393 ts-mocha --paths \"./test/*.ts\" --require \"./test/tracing.ts\" --timeout 10000", 10 | "test:jaeger": "OTEL_EXPORTER_JAEGER_AGENT_HOST=\"localhost\" OTEL_EXPORTER_JAEGER_AGENT_PORT=6832 yarn test" 11 | }, 12 | "dependencies": { 13 | "axios": "^0.21.1" 14 | }, 15 | "devDependencies": { 16 | "@types/expect": "^24.3.0", 17 | "@types/mocha": "^9.0.0", 18 | "chai": "^4.3.4", 19 | "malabi": "^0.0.7-alpha.1", 20 | "mocha": "^9.1.3", 21 | "ts-mocha": "^8.0.0", 22 | "typescript": "^4.2.4" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/tests-runner/test/service-under-test.spec.ts: -------------------------------------------------------------------------------- 1 | const SERVICE_UNDER_TEST_PORT = process.env.PORT || 8080; 2 | import { malabi } from 'malabi'; 3 | 4 | import { expect } from 'chai'; 5 | import axios from 'axios'; 6 | 7 | describe('testing service-under-test remotely', () => { 8 | 9 | it('successful /todo request', async () => { 10 | // get spans created from the previous call 11 | const telemetryRepo = await malabi( async () => { 12 | await axios(`http://localhost:${SERVICE_UNDER_TEST_PORT}/todo`); 13 | }); 14 | 15 | // Validate internal HTTP call 16 | const todoInternalHTTPCall = telemetryRepo.spans.outgoing().first; 17 | expect(todoInternalHTTPCall.httpFullUrl).equals('https://jsonplaceholder.typicode.com/todos/1') 18 | expect(todoInternalHTTPCall.statusCode).equals(200); 19 | }); 20 | 21 | it('successful /users request', async () => { 22 | // get spans created from the previous call 23 | const telemetryRepo = await malabi( async () => { 24 | // call to the service under test 25 | await axios.get(`http://localhost:${SERVICE_UNDER_TEST_PORT}/users`); 26 | }); 27 | 28 | // Validating that /users had ran a single select statement and responded with an array. 29 | const sequelizeActivities = telemetryRepo.spans.sequelize(); 30 | expect(sequelizeActivities.length).equals(1); 31 | expect(sequelizeActivities.first.dbOperation).equals("SELECT"); 32 | expect(Array.isArray(JSON.parse(sequelizeActivities.first.dbResponse))).equals(true); 33 | }); 34 | 35 | it('successful /users/Rick request', async () => { 36 | // get spans created from the previous call 37 | const telemetryRepo = await malabi( async () => { 38 | // call to the service under test 39 | await axios.get(`http://localhost:${SERVICE_UNDER_TEST_PORT}/users/Rick`); 40 | }); 41 | 42 | const sequelizeActivities = telemetryRepo.spans.sequelize(); 43 | expect(sequelizeActivities.length).equals(1); 44 | expect(sequelizeActivities.first.dbOperation).equals("SELECT"); 45 | const dbResponse = JSON.parse(sequelizeActivities.first.dbResponse); 46 | expect(Array.isArray(dbResponse)).equals(true); 47 | expect(dbResponse.length).equals(1); 48 | }); 49 | 50 | it('Non existing user - /users/Rick111 request', async () => { 51 | // get spans created from the previous call 52 | const telemetryRepo = await malabi( async () => { 53 | // call to the service under test 54 | await axios.get(`http://localhost:${SERVICE_UNDER_TEST_PORT}/users/Rick111`); 55 | }); 56 | 57 | const sequelizeActivities = telemetryRepo.spans.sequelize(); 58 | expect(sequelizeActivities.length).equals(1); 59 | expect(sequelizeActivities.first.dbOperation).equals("SELECT"); 60 | const dbResponse = JSON.parse(sequelizeActivities.first.dbResponse); 61 | expect(Array.isArray(dbResponse)).equals(true); 62 | expect(dbResponse.length).equals(0); 63 | expect(telemetryRepo.spans.httpGet().first.statusCode).equals(200); 64 | }); 65 | 66 | it('successful POST /users request', async () => { 67 | // get spans created from the previous call 68 | const telemetryRepo = await malabi( async () => { 69 | // call to the service under test 70 | const res = await axios.post(`http://localhost:${SERVICE_UNDER_TEST_PORT}/users`,{ 71 | firstName:'Morty', 72 | lastName:'Smith', 73 | }); 74 | expect(res.status).equals(200); 75 | }); 76 | 77 | // Validating that /users created a new record in DB 78 | const sequelizeActivities = telemetryRepo.spans.sequelize(); 79 | expect(sequelizeActivities.length).equals(1); 80 | expect(sequelizeActivities.first.dbOperation).equals("INSERT"); 81 | }); 82 | 83 | /* The expected flow is: 84 | 1) Insert into db the new user (due to first API call; POST /users). 85 | ------------------------------------------------------------------ 86 | 2) Try to fetch the user from Redis (due to second API call; GET /users/Jerry). 87 | 3) The user shouldn't be present in Redis so fetch from DB. 88 | 4) Push the user object from DB to Redis. 89 | */ 90 | it('successful create and fetch user', async () => { 91 | const telemetryRepo = await malabi( async () => { 92 | // Creating a new user 93 | const createUserResponse = await axios.post(`http://localhost:${SERVICE_UNDER_TEST_PORT}/users`,{ 94 | firstName:'Jerry', 95 | lastName:'Smith', 96 | }); 97 | expect(createUserResponse.status).equals(200); 98 | const fetchUserResponse = await axios.get(`http://localhost:${SERVICE_UNDER_TEST_PORT}/users/Jerry`); 99 | expect(fetchUserResponse.status).equals(200); 100 | }); 101 | 102 | const sequelizeActivities = telemetryRepo.spans.sequelize(); 103 | const redisActivities = telemetryRepo.spans.redis(); 104 | 105 | // 1) Insert into db the new user (due to first API call; POST /users). 106 | expect(sequelizeActivities.first.dbOperation).equals('INSERT'); 107 | // 2) Try to fetch the user from Redis (due to second API call; GET /users/Jerry). 108 | expect(redisActivities.first.dbStatement).equals("lrange Jerry 0 -1"); 109 | expect(redisActivities.first.dbResponse).equals("[]"); 110 | // 3) The user shouldn't be present in Redis so fetch from DB. 111 | expect(sequelizeActivities.second.dbOperation).equals("SELECT"); 112 | //4) Push the user object from DB to Redis. 113 | expect(redisActivities.second.dbStatement.startsWith('lpush Jerry')).equals(true); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /examples/tests-runner/test/tracing.ts: -------------------------------------------------------------------------------- 1 | import { instrument } from 'malabi'; 2 | 3 | instrument({ 4 | serviceName: 'tests-runner', 5 | }); 6 | -------------------------------------------------------------------------------- /examples/tests-runner/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "rootDir": ".", 4 | "outDir": "./dist", 5 | "lib": ["es2019"], 6 | "target": "ES5", 7 | "esModuleInterop": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true, 4 | "dictionaries": ["jsdoc", "closure"] 5 | }, 6 | "plugins": [ 7 | "node_modules/better-docs/typescript", 8 | "node_modules/better-docs/category" 9 | ], 10 | "source": { 11 | "include": ["."], 12 | "includePattern": "\\.(jsx|js|ts|tsx)$" 13 | }, 14 | "sourceType": "module", 15 | "opts": { 16 | "template": "node_modules/better-docs", 17 | "destination": "./docs/" 18 | }, 19 | "templates": { 20 | "search": true 21 | } 22 | } -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*", 4 | "examples/*" 5 | ], 6 | "version": "0.0.7-alpha.1", 7 | "npmClient": "yarn", 8 | "useWorkspaces": true 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "malabi", 3 | "version": "0.0.1", 4 | "description": "opentelemetry <-> testing integration for nodejs", 5 | "repository": "git@github.com:aspecto-io/malabi.git", 6 | "author": { 7 | "name": "Aspecto", 8 | "email": "support@aspecto.io", 9 | "url": "https://aspecto.io" 10 | }, 11 | "license": "Apache-2.0", 12 | "private": true, 13 | "scripts": { 14 | "build": "lerna run build", 15 | "watch": "lerna run --parallel watch", 16 | "test": "lerna run test", 17 | "prettier": "prettier --config .prettierrc.yml --write --ignore-unknown \"**/*\"" 18 | }, 19 | "devDependencies": { 20 | "jsdoc": "^3.6.7", 21 | "lerna": "^4.0.0", 22 | "prettier": "^2.2.1", 23 | "typedoc": "^0.22.12", 24 | "typedoc-plugin-missing-exports": "^0.22.6" 25 | }, 26 | "workspaces": [ 27 | "packages/*", 28 | "examples/*" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /packages/instrumentation-node/README.md: -------------------------------------------------------------------------------- 1 | # malabi-instrumentation-node 2 | 3 | Automatic instrumentation for nodejs for the malabi project. 4 | 5 | ## Configuration 6 | The configuration for this package is global to all instrumentations. 7 | 8 | | Options | Type | Default | Description | 9 | | -------------- | -------------------------------------- | --- | ----------------------------------------------------------------------------------------------- | 10 | | `collectPayloads` | `boolean` | false | Collect operations payloads (request and response) when possible 11 | | `suppressInternalInstrumentation` | `boolean` | true | Don't collect spans for internal implementation operations of instrumented packages | 12 | --- 13 | 14 | ## Instrumentations 15 | Other instrumentations installed via this package: 16 | - [http](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-instrumentation-http) 17 | - [ioredis](https://github.com/open-telemetry/opentelemetry-js-contrib/tree/main/plugins/node/opentelemetry-instrumentation-ioredis) 18 | 19 | TODO: add the instrumentations from ext-js -------------------------------------------------------------------------------- /packages/instrumentation-node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "malabi-instrumentation-node", 3 | "version": "0.0.7-alpha.1", 4 | "description": "open telemetry auto instrumentation for the nodejs", 5 | "keywords": [ 6 | "opentelemetry", 7 | "nodejs" 8 | ], 9 | "homepage": "https://github.com/aspecto-io/opentelemetry-ext-js", 10 | "license": "Apache-2.0", 11 | "main": "dist/src/index.js", 12 | "files": [ 13 | "dist/src/**/*.js", 14 | "dist/src/**/*.d.ts", 15 | "LICENSE", 16 | "README.md" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/aspecto-io/opentelemetry-ext-js.git" 21 | }, 22 | "scripts": { 23 | "build": "tsc", 24 | "prepare": "yarn run build", 25 | "test": "mocha", 26 | "watch": "tsc -w" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/aspecto-io/opentelemetry-ext-js/issues" 30 | }, 31 | "dependencies": { 32 | "@opentelemetry/instrumentation-mongodb": "0.43.0", 33 | "@opentelemetry/api": "^1.8.0", 34 | "@opentelemetry/instrumentation-aws-sdk": "0.41.0", 35 | "@opentelemetry/instrumentation-http": "0.51.1", 36 | "@opentelemetry/instrumentation-ioredis": "0.40.0", 37 | "@opentelemetry/instrumentation-mongoose": "0.38.1", 38 | "opentelemetry-instrumentation-elasticsearch": "0.40.0", 39 | "opentelemetry-instrumentation-express": "0.40.0", 40 | "opentelemetry-instrumentation-kafkajs": "0.40.0", 41 | "opentelemetry-instrumentation-neo4j": "0.40.0", 42 | "opentelemetry-instrumentation-sequelize": "0.40.0", 43 | "opentelemetry-instrumentation-typeorm": "0.40.0" 44 | }, 45 | "devDependencies": { 46 | "@opentelemetry/instrumentation": "0.51.1", 47 | "@opentelemetry/sdk-trace-base": "^1.24.1", 48 | "@opentelemetry/sdk-trace-node": "^1.24.1", 49 | "@types/amqplib": "^0.8.1", 50 | "axios": "^0.21.1", 51 | "expect": "^26.6.2", 52 | "mocha": "^8.3.2", 53 | "ts-node": "^10.9.2", 54 | "typescript": "^4.0.5" 55 | }, 56 | "mocha": { 57 | "extension": [ 58 | "ts" 59 | ], 60 | "spec": "test/**/*.spec.ts", 61 | "require": "ts-node/register" 62 | }, 63 | "gitHead": "0ee1da82de292ead797f53c05591eaadfb9a7948" 64 | } 65 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/enums.ts: -------------------------------------------------------------------------------- 1 | export enum GeneralExtendedAttribute { 2 | INSTRUMENTED_LIBRARY_VERSION = 'instrumented_library.version', 3 | } 4 | 5 | export enum MessagingExtendedAttribute { 6 | MESSAGING_PAYLOAD = 'messaging.payload', 7 | MESSAGING_RABBITMQ_CONSUME_END_OPERATION = 'messaging.rabbitmq.consume_end_operation', 8 | } 9 | 10 | export enum DbExtendedAttribute { 11 | DB_RESPONSE = 'db.response', 12 | } 13 | 14 | export enum HttpExtendedAttribute { 15 | HTTP_REQUEST_HEADERS = 'http.request.headers', 16 | HTTP_REQUEST_BODY = 'http.request.body', 17 | HTTP_RESPONSE_HEADERS = 'http.response.headers', 18 | HTTP_RESPONSE_BODY = 'http.response.body', 19 | HTTP_PATH = 'http.path', 20 | } 21 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/index.ts: -------------------------------------------------------------------------------- 1 | export { getNodeAutoInstrumentations } from './instrumentations-config'; 2 | export * from './types'; 3 | export * from './enums'; 4 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/instrumentations-config/aws-sdk.ts: -------------------------------------------------------------------------------- 1 | import { Span } from '@opentelemetry/api'; 2 | import { callHookOnlyOnRecordingSpan } from '../payload-collection/recording-span'; 3 | import { 4 | AwsSdkInstrumentationConfig, 5 | NormalizedRequest, 6 | NormalizedResponse, 7 | } from '@opentelemetry/instrumentation-aws-sdk'; 8 | import { DbExtendedAttribute, MessagingExtendedAttribute } from '../enums'; 9 | import { AutoInstrumentationOptions } from '../types'; 10 | 11 | const enum AttributeNames { 12 | AWS_REQUEST_PARAMS = 'aws.request.params', 13 | } 14 | 15 | const whiteListParams: Record = { 16 | sqs: ['QueueUrl', 'DelaySeconds', 'MaxNumberOfMessages', 'WaitTimeSeconds'], 17 | s3: ['Bucket', 'Key', 'ACL', 'ContentType', 'ResponseContentType'], 18 | sns: ['TopicArn'], 19 | kinesis: ['StreamName', 'PartitionKey'], 20 | firehose: ['DeliveryStreamName'], 21 | ebs: ['SnapshotId'], 22 | ssm: ['Name'], 23 | lambda: ['FunctionName'], 24 | athena: ['WorkGroup', 'QueryString'], 25 | sts: ['RoleArn'], 26 | }; 27 | 28 | const getRequestWhitelistedParams = (serviceName: string, requestParams: Record): Record => { 29 | const paramsToCapture: string[] = whiteListParams[serviceName]; 30 | if (!paramsToCapture || !requestParams) return; 31 | 32 | return paramsToCapture.reduce((whiteListParams: Record, currParamName: string) => { 33 | const val = requestParams[currParamName]; 34 | if (val !== undefined) { 35 | whiteListParams[currParamName] = val; 36 | } 37 | return whiteListParams; 38 | }, {}); 39 | }; 40 | 41 | const addSqsPayload = (span: Span, request: NormalizedRequest) => { 42 | let payload; 43 | switch (request.commandName) { 44 | case 'sendMessage': { 45 | payload = request.commandInput?.MessageBody; 46 | if (typeof payload !== 'string') return; 47 | break; 48 | } 49 | 50 | case 'sendMessageBatch': { 51 | let messagesPayload = request.commandInput?.Entries?.map((entry) => ({ 52 | msgId: entry.Id, 53 | payload: entry.MessageBody, 54 | })); 55 | try { 56 | payload = JSON.stringify(messagesPayload); 57 | } catch {} 58 | break; 59 | } 60 | } 61 | 62 | if (payload === undefined) return; 63 | span.setAttribute(MessagingExtendedAttribute.MESSAGING_PAYLOAD, payload); 64 | }; 65 | 66 | const awsSdkRequestHook = (options: AutoInstrumentationOptions) => (span: Span, request: NormalizedRequest) => { 67 | const paramsToAttach = getRequestWhitelistedParams(request.serviceName, request.commandInput); 68 | if (paramsToAttach) { 69 | try { 70 | span.setAttribute(AttributeNames.AWS_REQUEST_PARAMS, JSON.stringify(paramsToAttach)); 71 | } catch {} 72 | } 73 | 74 | switch (request.serviceName) { 75 | case 'sqs': 76 | if (options.collectPayloads) { 77 | addSqsPayload(span, request); 78 | } 79 | break; 80 | } 81 | }; 82 | 83 | const awsSdkResponseHook = (span: Span, response: NormalizedResponse) => { 84 | if (response.request.serviceName === 'dynamodb') { 85 | if (typeof response.data !== 'object') return; 86 | span.setAttribute(DbExtendedAttribute.DB_RESPONSE, JSON.stringify(response.data)); 87 | } 88 | }; 89 | 90 | interface SqsMessage { 91 | Body?: string; 92 | } 93 | 94 | const sqsProcessCapturePayload = (span: Span, message: SqsMessage) => { 95 | if (message.Body === undefined) return; 96 | span.setAttribute(MessagingExtendedAttribute.MESSAGING_PAYLOAD, message.Body); 97 | }; 98 | 99 | export const awsSdkInstrumentationConfig = (options: AutoInstrumentationOptions): AwsSdkInstrumentationConfig => ({ 100 | preRequestHook: callHookOnlyOnRecordingSpan(awsSdkRequestHook(options)), 101 | responseHook: options.collectPayloads && callHookOnlyOnRecordingSpan(awsSdkResponseHook), 102 | sqsProcessHook: options.collectPayloads && callHookOnlyOnRecordingSpan(sqsProcessCapturePayload), 103 | suppressInternalInstrumentation: options.suppressInternalInstrumentation, 104 | }); 105 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/instrumentations-config/elasticsearch.ts: -------------------------------------------------------------------------------- 1 | import { Span } from '@opentelemetry/api'; 2 | import { ElasticsearchInstrumentationConfig, ResponseHook } from 'opentelemetry-instrumentation-elasticsearch'; 3 | import { DbExtendedAttribute, GeneralExtendedAttribute } from '../enums'; 4 | import { AutoInstrumentationOptions } from '../types'; 5 | import { callHookOnlyOnRecordingSpan } from '../payload-collection/recording-span'; 6 | 7 | const responseHook: ResponseHook = (span: Span, response: any) => { 8 | span.setAttribute(DbExtendedAttribute.DB_RESPONSE, JSON.stringify(response)); 9 | }; 10 | 11 | export const elasticsearchInstrumentationConfig = ( 12 | options: AutoInstrumentationOptions 13 | ): ElasticsearchInstrumentationConfig => ({ 14 | moduleVersionAttributeName: GeneralExtendedAttribute.INSTRUMENTED_LIBRARY_VERSION, 15 | suppressInternalInstrumentation: options.suppressInternalInstrumentation, 16 | responseHook: options.collectPayloads && callHookOnlyOnRecordingSpan(responseHook), 17 | }); 18 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/instrumentations-config/express.ts: -------------------------------------------------------------------------------- 1 | import type { Request, Response } from 'express'; 2 | import { ExpressInstrumentationConfig } from 'opentelemetry-instrumentation-express'; 3 | import { callHookOnlyOnRecordingSpan } from '../payload-collection/recording-span'; 4 | import { Span } from '@opentelemetry/api'; 5 | import { HttpExtendedAttribute } from '../enums'; 6 | import { shouldCaptureBodyByMimeType } from '../payload-collection/mime-type'; 7 | import { StreamChunks } from '../payload-collection/stream-chunks'; 8 | import { AutoInstrumentationOptions } from '../types'; 9 | 10 | export const requestHook = (options: AutoInstrumentationOptions) => (span: Span, req: Request, res: Response) => { 11 | span.setAttributes({ 12 | [HttpExtendedAttribute.HTTP_PATH]: req.path, 13 | [HttpExtendedAttribute.HTTP_REQUEST_HEADERS]: JSON.stringify(req.headers), 14 | }); 15 | 16 | const requestMimeType = req.get('content-type'); 17 | const captureRequestBody = options.collectPayloads && shouldCaptureBodyByMimeType(requestMimeType); 18 | const requestStreamChunks = new StreamChunks(); 19 | 20 | if (captureRequestBody) { 21 | req.on('data', (chunk) => requestStreamChunks.addChunk(chunk)); 22 | } 23 | 24 | const responseStreamChunks = new StreamChunks(); 25 | 26 | if (options.collectPayloads) { 27 | const originalResWrite = res.write; 28 | 29 | (res as any).write = function (chunk: any) { 30 | responseStreamChunks.addChunk(chunk); 31 | originalResWrite.apply(res, arguments); 32 | }; 33 | } 34 | 35 | const oldResEnd = res.end; 36 | res.end = function (chunk: any) { 37 | oldResEnd.apply(res, arguments); 38 | 39 | const responseMimeType = res.get('content-type'); 40 | const captureResponseBody = options.collectPayloads && shouldCaptureBodyByMimeType(responseMimeType); 41 | if (captureResponseBody) responseStreamChunks.addChunk(chunk); 42 | 43 | if (options.collectPayloads) { 44 | span.setAttributes({ 45 | [HttpExtendedAttribute.HTTP_REQUEST_BODY]: captureRequestBody 46 | ? requestStreamChunks.getBody() 47 | : `Request body not collected due to unsupported mime type: ${requestMimeType}`, 48 | [HttpExtendedAttribute.HTTP_RESPONSE_BODY]: captureResponseBody 49 | ? responseStreamChunks.getBody() 50 | : `Response body not collected due to unsupported mime type: ${responseMimeType}`, 51 | }); 52 | } 53 | 54 | span.setAttributes({ 55 | [HttpExtendedAttribute.HTTP_RESPONSE_HEADERS]: JSON.stringify(res.getHeaders()), 56 | }); 57 | }; 58 | }; 59 | 60 | export const expressInstrumentationConfig = (options: AutoInstrumentationOptions): ExpressInstrumentationConfig => ({ 61 | requestHook: callHookOnlyOnRecordingSpan(requestHook(options)), 62 | }); 63 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/instrumentations-config/http.ts: -------------------------------------------------------------------------------- 1 | import { Span } from '@opentelemetry/api'; 2 | import { HttpInstrumentationConfig } from '@opentelemetry/instrumentation-http'; 3 | import { IncomingMessage, ServerResponse, ClientRequest } from 'http'; 4 | import { HttpExtendedAttribute } from '../enums'; 5 | import { shouldCaptureBodyByMimeType } from '../payload-collection/mime-type'; 6 | import { StreamChunks } from '../payload-collection/stream-chunks'; 7 | import { AutoInstrumentationOptions } from '../types'; 8 | import { callHookOnlyOnRecordingSpan } from '../payload-collection/recording-span'; 9 | 10 | const streamChunksKey = Symbol('opentelemetry.instrumentation.http.StreamChunks'); 11 | 12 | const httpCustomAttributes = ( 13 | span: Span, 14 | request: ClientRequest | IncomingMessage, 15 | response: IncomingMessage | ServerResponse 16 | ): void => { 17 | if (request instanceof ClientRequest) { 18 | const reqPath = request.path.split('?')[0]; 19 | span.setAttribute(HttpExtendedAttribute.HTTP_PATH, reqPath); 20 | span.setAttribute( 21 | HttpExtendedAttribute.HTTP_REQUEST_HEADERS, 22 | JSON.stringify((request as ClientRequest).getHeaders()) 23 | ); 24 | } 25 | if (response instanceof IncomingMessage) { 26 | span.setAttribute( 27 | HttpExtendedAttribute.HTTP_RESPONSE_HEADERS, 28 | JSON.stringify((response as IncomingMessage).headers) 29 | ); 30 | } 31 | 32 | const requestBody: StreamChunks = request[streamChunksKey]; 33 | if (requestBody) { 34 | span.setAttribute(HttpExtendedAttribute.HTTP_REQUEST_BODY, requestBody.getBody()); 35 | } 36 | 37 | const responseBody: StreamChunks = response[streamChunksKey]; 38 | if (responseBody) { 39 | span.setAttribute(HttpExtendedAttribute.HTTP_RESPONSE_BODY, responseBody.getBody()); 40 | } 41 | }; 42 | 43 | const httpCustomAttributesOnRequest = (span: Span, request: ClientRequest | IncomingMessage): void => { 44 | if (request instanceof ClientRequest) { 45 | const requestMimeType = request.getHeader('content-type') as string; 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: any) { 57 | const aspectoData: StreamChunks = request[streamChunksKey]; 58 | aspectoData?.addChunk(data); 59 | return oldWrite.call(request, data); 60 | }; 61 | } 62 | }; 63 | 64 | const httpCustomAttributesOnResponse = (span: Span, response: IncomingMessage | ServerResponse): void => { 65 | if (response instanceof IncomingMessage) { 66 | const responseMimeType = response.headers?.['content-type'] as string; 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: any) { 78 | if (chunk) { 79 | const aspectoData: StreamChunks = response[streamChunksKey]; 80 | aspectoData?.addChunk(chunk); 81 | } 82 | return origPush.apply(this, arguments); 83 | }; 84 | } 85 | }; 86 | 87 | export const httpInstrumentationConfig = (options: AutoInstrumentationOptions): HttpInstrumentationConfig => ({ 88 | applyCustomAttributesOnSpan: callHookOnlyOnRecordingSpan(httpCustomAttributes), 89 | requestHook: options.collectPayloads && callHookOnlyOnRecordingSpan(httpCustomAttributesOnRequest), 90 | responseHook: options.collectPayloads && callHookOnlyOnRecordingSpan(httpCustomAttributesOnResponse), 91 | }); 92 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/instrumentations-config/index.ts: -------------------------------------------------------------------------------- 1 | // Configs 2 | import { kafkaJsInstrumentationConfig } from './kafkajs'; 3 | import { awsSdkInstrumentationConfig } from './aws-sdk'; 4 | import { typeormInstrumentationConfig, sequelizeInstrumentationConfig } from './orm'; 5 | import { ioredisInstrumentationConfig } from './ioredis'; 6 | import { httpInstrumentationConfig } from './http'; 7 | import { mongooseInstrumentationConfig } from './mongoose'; 8 | import { elasticsearchInstrumentationConfig } from './elasticsearch'; 9 | import { expressInstrumentationConfig } from './express'; 10 | import { neo4jInstrumentationConfig } from './neo4j'; 11 | 12 | // Instrumentations 13 | import { HttpInstrumentation } from '@opentelemetry/instrumentation-http'; 14 | import { IORedisInstrumentation } from '@opentelemetry/instrumentation-ioredis'; 15 | import { ExpressInstrumentation } from 'opentelemetry-instrumentation-express'; 16 | import { SequelizeInstrumentation } from 'opentelemetry-instrumentation-sequelize'; 17 | import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk'; 18 | import { TypeormInstrumentation } from 'opentelemetry-instrumentation-typeorm'; 19 | import { KafkaJsInstrumentation } from 'opentelemetry-instrumentation-kafkajs'; 20 | import { MongooseInstrumentation } from '@opentelemetry/instrumentation-mongoose'; 21 | import { ElasticsearchInstrumentation } from 'opentelemetry-instrumentation-elasticsearch'; 22 | import { Neo4jInstrumentation } from 'opentelemetry-instrumentation-neo4j'; 23 | import { AutoInstrumentationOptions } from '../types'; 24 | import { InstrumentationOption } from '@opentelemetry/instrumentation'; 25 | 26 | const DEFAULT_OPTIONS: AutoInstrumentationOptions = { 27 | collectPayloads: false, 28 | suppressInternalInstrumentation: true, 29 | }; 30 | 31 | export const getNodeAutoInstrumentations = (options?: AutoInstrumentationOptions): InstrumentationOption[] => { 32 | const optionsWithDefaults = { ...DEFAULT_OPTIONS, ...options }; 33 | return [ 34 | new ExpressInstrumentation(expressInstrumentationConfig(optionsWithDefaults)), 35 | new SequelizeInstrumentation(sequelizeInstrumentationConfig(optionsWithDefaults)), 36 | new KafkaJsInstrumentation(kafkaJsInstrumentationConfig(optionsWithDefaults)), 37 | new AwsInstrumentation(awsSdkInstrumentationConfig(optionsWithDefaults)), 38 | new TypeormInstrumentation(typeormInstrumentationConfig(optionsWithDefaults)), 39 | new MongooseInstrumentation(mongooseInstrumentationConfig(optionsWithDefaults)), 40 | new ElasticsearchInstrumentation(elasticsearchInstrumentationConfig(optionsWithDefaults)), 41 | new HttpInstrumentation(httpInstrumentationConfig(optionsWithDefaults)), 42 | new Neo4jInstrumentation(neo4jInstrumentationConfig(optionsWithDefaults)), 43 | new IORedisInstrumentation(ioredisInstrumentationConfig(optionsWithDefaults)), 44 | ]; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/instrumentations-config/ioredis.ts: -------------------------------------------------------------------------------- 1 | import { SpanAttributeValue, Span } from '@opentelemetry/api'; 2 | import { callHookOnlyOnRecordingSpan } from '../payload-collection/recording-span'; 3 | import { IORedisInstrumentationConfig } from '@opentelemetry/instrumentation-ioredis'; 4 | import { DbExtendedAttribute, GeneralExtendedAttribute } from '../enums'; 5 | import { getModuleVersion } from './module-version'; 6 | import { AutoInstrumentationOptions } from '../types'; 7 | 8 | const ioredisCustomAttributesOnResponse = 9 | (options: AutoInstrumentationOptions) => 10 | (span: Span, _cmdName: string, _cmdArgs: Array, response: any): void => { 11 | if (options.collectPayloads) { 12 | let responsePayload: SpanAttributeValue; 13 | if (typeof response === 'string' || typeof response === 'number') { 14 | responsePayload = response; 15 | } else if (response instanceof Buffer) { 16 | responsePayload = response.toString(); 17 | } else if (typeof response === 'object') { 18 | responsePayload = JSON.stringify(response); 19 | } 20 | 21 | if (responsePayload !== undefined) { 22 | span.setAttribute(DbExtendedAttribute.DB_RESPONSE, responsePayload); 23 | } 24 | } 25 | 26 | const version = getModuleVersion('ioredis'); 27 | if (version) { 28 | span.setAttribute(GeneralExtendedAttribute.INSTRUMENTED_LIBRARY_VERSION, version); 29 | } 30 | }; 31 | 32 | export const ioredisInstrumentationConfig = (options: AutoInstrumentationOptions): IORedisInstrumentationConfig => ({ 33 | responseHook: callHookOnlyOnRecordingSpan(ioredisCustomAttributesOnResponse(options)), 34 | }); 35 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/instrumentations-config/kafkajs.ts: -------------------------------------------------------------------------------- 1 | import { Span } from '@opentelemetry/api'; 2 | import { callHookOnlyOnRecordingSpan } from '../payload-collection/recording-span'; 3 | import { KafkaJsInstrumentationConfig } from 'opentelemetry-instrumentation-kafkajs'; 4 | import { GeneralExtendedAttribute, MessagingExtendedAttribute } from '../enums'; 5 | import { AutoInstrumentationOptions } from '../types'; 6 | 7 | export interface KafkaJsMessage { 8 | value: Buffer | string | null; 9 | } 10 | 11 | const addPayloadHook = (span: Span, _topic: string, message: KafkaJsMessage) => { 12 | if (message.value !== null && message.value !== undefined) { 13 | span.setAttribute(MessagingExtendedAttribute.MESSAGING_PAYLOAD, message.value.toString()); 14 | } 15 | }; 16 | 17 | export const kafkaJsInstrumentationConfig = (options: AutoInstrumentationOptions): KafkaJsInstrumentationConfig => ({ 18 | producerHook: options.collectPayloads && callHookOnlyOnRecordingSpan(addPayloadHook), 19 | consumerHook: options.collectPayloads && callHookOnlyOnRecordingSpan(addPayloadHook), 20 | moduleVersionAttributeName: GeneralExtendedAttribute.INSTRUMENTED_LIBRARY_VERSION, 21 | }); 22 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/instrumentations-config/module-version.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | 4 | const cache: Record = {}; 5 | 6 | export const getModuleVersion = (moduleName: string) => { 7 | if (cache[moduleName]) return cache[moduleName]; 8 | try { 9 | const json = JSON.parse(fs.readFileSync(path.join('node_modules', moduleName, 'package.json'), 'utf8')); 10 | cache[moduleName] = json.version; 11 | } catch (err) { 12 | cache[moduleName] = undefined; 13 | } 14 | return cache[moduleName]; 15 | }; 16 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/instrumentations-config/mongoose.ts: -------------------------------------------------------------------------------- 1 | import { Span } from '@opentelemetry/api'; 2 | import { 3 | MongooseInstrumentationConfig, 4 | SerializerPayload, 5 | MongooseResponseCustomAttributesFunction, 6 | DbStatementSerializer, 7 | } from '@opentelemetry/instrumentation-mongoose'; 8 | import { callHookOnlyOnRecordingSpan } from '../payload-collection/recording-span'; 9 | import { DbExtendedAttribute, GeneralExtendedAttribute } from '../enums'; 10 | import { AutoInstrumentationOptions } from '../types'; 11 | 12 | const isEmpty = (obj: any) => !obj || (typeof obj === 'object' && Object.keys(obj).length === 0); 13 | 14 | const dbStatementSerializer: DbStatementSerializer = (_op: string, payload: SerializerPayload) => { 15 | try { 16 | if (isEmpty(payload.options)) delete payload.options; 17 | return JSON.stringify(payload); 18 | } catch { 19 | return undefined; 20 | } 21 | }; 22 | 23 | const responseHook: MongooseResponseCustomAttributesFunction = (span: Span, response: any) => { 24 | span.setAttribute(DbExtendedAttribute.DB_RESPONSE, JSON.stringify(response)); 25 | }; 26 | 27 | export const mongooseInstrumentationConfig = (options: AutoInstrumentationOptions): MongooseInstrumentationConfig => ({ 28 | suppressInternalInstrumentation: options.suppressInternalInstrumentation, 29 | responseHook: options.collectPayloads && callHookOnlyOnRecordingSpan(responseHook), 30 | dbStatementSerializer 31 | }); 32 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/instrumentations-config/neo4j.ts: -------------------------------------------------------------------------------- 1 | import { Neo4jInstrumentationConfig } from 'opentelemetry-instrumentation-neo4j'; 2 | import { DbExtendedAttribute, GeneralExtendedAttribute } from '../enums'; 3 | import { AutoInstrumentationOptions } from '../types'; 4 | import { callHookOnlyOnRecordingSpan } from '../payload-collection/recording-span'; 5 | 6 | export const neo4jInstrumentationConfig = (options: AutoInstrumentationOptions): Neo4jInstrumentationConfig => ({ 7 | ignoreOrphanedSpans: true, 8 | moduleVersionAttributeName: GeneralExtendedAttribute.INSTRUMENTED_LIBRARY_VERSION, 9 | responseHook: 10 | options.collectPayloads && 11 | callHookOnlyOnRecordingSpan((span, response) => { 12 | span.setAttribute( 13 | DbExtendedAttribute.DB_RESPONSE, 14 | JSON.stringify(response.records.map((r) => r.toObject())) 15 | ); 16 | }), 17 | }); 18 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/instrumentations-config/orm.ts: -------------------------------------------------------------------------------- 1 | import { Span } from '@opentelemetry/api'; 2 | import { SequelizeInstrumentationConfig } from 'opentelemetry-instrumentation-sequelize'; 3 | import { TypeormInstrumentationConfig } from 'opentelemetry-instrumentation-typeorm'; 4 | import { DbExtendedAttribute, GeneralExtendedAttribute } from '../enums'; 5 | import { AutoInstrumentationOptions } from '../types'; 6 | import { callHookOnlyOnRecordingSpan } from '../payload-collection/recording-span'; 7 | 8 | const addResponsePayload = (span: Span, response: any) => { 9 | const stringified = JSON.stringify(response); 10 | if (!stringified) return; 11 | 12 | const binarySize = Buffer.byteLength(stringified, 'utf8'); 13 | // Limit to 0.5MB. 14 | if (binarySize > 500000) return; 15 | 16 | span.setAttribute(DbExtendedAttribute.DB_RESPONSE, stringified); 17 | }; 18 | 19 | export const typeormInstrumentationConfig = (options: AutoInstrumentationOptions): TypeormInstrumentationConfig => ({ 20 | responseHook: options.collectPayloads && callHookOnlyOnRecordingSpan(addResponsePayload), 21 | moduleVersionAttributeName: GeneralExtendedAttribute.INSTRUMENTED_LIBRARY_VERSION, 22 | }); 23 | 24 | export const sequelizeInstrumentationConfig = ( 25 | options: AutoInstrumentationOptions 26 | ): SequelizeInstrumentationConfig => ({ 27 | responseHook: options.collectPayloads && callHookOnlyOnRecordingSpan(addResponsePayload), 28 | moduleVersionAttributeName: GeneralExtendedAttribute.INSTRUMENTED_LIBRARY_VERSION, 29 | ignoreOrphanedSpans: true, 30 | }); 31 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/payload-collection/mime-type.ts: -------------------------------------------------------------------------------- 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: string) => { 13 | try { 14 | return !mimeType || allowedMimeTypePrefix.some((prefix) => mimeType.startsWith(prefix)); 15 | } catch { 16 | return true; 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/payload-collection/recording-span.ts: -------------------------------------------------------------------------------- 1 | import { Span } from '@opentelemetry/api'; 2 | 3 | export type InstrumentationHookFunction = (span: Span, ...args: any[]) => any; 4 | 5 | export const callHookOnlyOnRecordingSpan = (hookFunction: InstrumentationHookFunction): InstrumentationHookFunction => { 6 | return (span: Span, ...args) => { 7 | if (!span.isRecording()) return; 8 | return hookFunction(span, ...args); 9 | }; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/payload-collection/stream-chunks.ts: -------------------------------------------------------------------------------- 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: String[] | Buffer[]; 8 | length: number; 9 | 10 | constructor() { 11 | this.chunks = []; 12 | this.length = 0; 13 | } 14 | 15 | addChunk(chunk: any) { 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(): string { 26 | return this.chunks.join(''); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/instrumentation-node/src/types.ts: -------------------------------------------------------------------------------- 1 | export interface AutoInstrumentationOptions { 2 | /** 3 | * Collect operations payloads (request and response) when possible. 4 | * Setting this option to true increases observability with the cost of more resources (memory, cpu, network) 5 | * 6 | * Default: false 7 | * */ 8 | collectPayloads?: boolean; 9 | 10 | /** 11 | * Don't collect spans for internal implementation operations of instrumented packages. 12 | * For example, aws-sdk is using http under the hood, which can be noisy for the user which already has the data in higher level. 13 | * Internal implementation spans are sometimes interesting, but mostly don't. 14 | * 15 | * Setting this option to false will uncover more of what your application is actually doing in low level implementation, 16 | * with the cost of larger traces and more resources (memory, cpu, network), 17 | * 18 | * Default: true 19 | */ 20 | suppressInternalInstrumentation?: boolean; 21 | } 22 | -------------------------------------------------------------------------------- /packages/instrumentation-node/test/auto-instrumentation.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import expect from 'expect'; 3 | import { memoryExporter, registerAutoInstrumentationsForTests } from './instrument'; 4 | 5 | registerAutoInstrumentationsForTests({}); 6 | import axios from 'axios'; 7 | import { HttpExtendedAttribute } from '../src'; 8 | 9 | describe('auto-instrumentation', () => { 10 | describe('http', () => { 11 | it('outgoing http', async () => { 12 | await axios.get('https://jsonplaceholder.typicode.com/todos/1'); 13 | const outgoingHttpSpan = memoryExporter.getFinishedSpans()[0]; 14 | expect(outgoingHttpSpan).toBeDefined(); 15 | expect(outgoingHttpSpan.instrumentationLibrary.name).toBe('@opentelemetry/instrumentation-http'); 16 | 17 | expect(outgoingHttpSpan.attributes[HttpExtendedAttribute.HTTP_PATH]).toBe('/todos/1'); 18 | 19 | const requestHeaders = JSON.parse( 20 | outgoingHttpSpan.attributes[HttpExtendedAttribute.HTTP_REQUEST_HEADERS] as string 21 | ); 22 | expect(requestHeaders['traceparent']).toMatch(/^[0-9a-f]{2}-[0-9a-f]{32}-[0-9a-f]{16}-[0-9a-f]{2}$/i); 23 | 24 | const responseHeaders = JSON.parse( 25 | outgoingHttpSpan.attributes[HttpExtendedAttribute.HTTP_RESPONSE_HEADERS] as string 26 | ); 27 | expect(responseHeaders).toBeDefined(); 28 | 29 | // we do not collect payloads 30 | expect(outgoingHttpSpan.attributes[HttpExtendedAttribute.HTTP_REQUEST_BODY]).toBeUndefined(); 31 | expect(outgoingHttpSpan.attributes[HttpExtendedAttribute.HTTP_RESPONSE_BODY]).toBeUndefined(); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/instrumentation-node/test/instrument.ts: -------------------------------------------------------------------------------- 1 | import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; 2 | import { InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; 3 | import { registerInstrumentations } from '@opentelemetry/instrumentation'; 4 | import { AutoInstrumentationOptions, getNodeAutoInstrumentations } from '../src'; 5 | 6 | export const memoryExporter = new InMemorySpanExporter(); 7 | const provider = new NodeTracerProvider(); 8 | provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); 9 | provider.register(); 10 | 11 | export const registerAutoInstrumentationsForTests = (options: AutoInstrumentationOptions) => { 12 | registerInstrumentations({ instrumentations: getNodeAutoInstrumentations(options) }); 13 | }; 14 | -------------------------------------------------------------------------------- /packages/instrumentation-node/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src/**/*.ts", "test/**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/malabi/README.md: -------------------------------------------------------------------------------- 1 |

2 | Malabi 3 | 4 |

5 |

6 | OpenTelemetry based Javascript test framework 7 |

8 | 9 | 10 | Malabi is released under the Apache-2.0 license. 11 | 12 | 13 | # Description 14 | This library introduces a new way of testing services: **Trace-based testing** (TBT). It is very useful when you want to validate integration between different parts. For example: make sure elasticsearch received the correct params on insert. 15 | 16 | - 💻 **Developer friendly**: Built by developers, for developers who love distributed applications. 17 | 18 | - ✅ **Validate integration**: Access to validate any backend interaction, fast, simple and reliable. 19 | 20 | - 🔗 **OpenTelemetry based**: Built based on OpenTelemetry to match the characteristics of distributed apps. 21 | 22 | 23 | ## How it works 24 | How it work diagram 25 | 26 | There are two main components to Malabi: 27 | 28 | 1. An OpenTelemetry SDK Distribution - used to collect any activity in the service under test by instrumenting it. **It is stored in the memory of the asserted service or in a Jaeger instance **, and exposes and endpoint for the test runner to access & make assertions. 29 | 30 | 2. An assertion library for OpenTelemetry data - by using the `malabi` wrapper function you will get access to any span created by the current test, then you will be able to validate the span and the service behavior 31 | 32 | ## Getting started 33 | ### In the microservice you want to test 34 | 1. ```npm install --save malabi``` or ```yarn add malabi``` 35 | 2. Add the following code at the service initialization, for example: in index.js. needs to be before any other imports to work properly. 36 | ```JS 37 | import { instrument, serveMalabiFromHttpApp } from 'malabi'; 38 | const instrumentationConfig = { 39 | serviceName: 'service-under-test', 40 | }; 41 | instrument(instrumentationConfig); 42 | serveMalabiFromHttpApp(18393, instrumentationConfig); 43 | 44 | import axios from 'axios'; 45 | import express from 'express'; 46 | import User from "./db"; 47 | const PORT = process.env.PORT || 8080; 48 | 49 | const app = express(); 50 | 51 | app.get('/todo', async (req, res) => { 52 | try { 53 | const todoItem = await axios('https://jsonplaceholder.typicode.com/todos/1'); 54 | res.json({ 55 | title: todoItem.data.title, 56 | }); 57 | } catch (e) { 58 | res.sendStatus(500); 59 | console.error(e, e); 60 | } 61 | }); 62 | ``` 63 | 64 | ## In your test file 65 | Create a tracing.ts file to set up instrumentation on the tests runner(this enables us to separate spans created in one test from other tests' spans from the other): 66 | ```JS 67 | import { instrument } from 'malabi'; 68 | 69 | instrument({ 70 | serviceName: 'tests-runner', 71 | }); 72 | ``` 73 | 74 | And this is how the test file looks like(service-under-test.spec.ts): 75 | Note: this should be run with node --require. 76 | 77 | Also, you must provide the MALABI_ENDPOINT_PORT_OR_URL env var (must start with http for url) 78 | ``` 79 | MALABI_ENDPOINT_PORT_OR_URL=http://localhost:18393 ts-mocha --paths "./test/*.ts" --require "./test/tracing.ts" 80 | ``` 81 | Or alternatively just with port(assuming localhost by default): 82 | ``` 83 | MALABI_ENDPOINT_PORT_OR_URL=18393 ts-mocha --paths "./test/*.ts" --require "./test/tracing.ts" 84 | ``` 85 | 86 | Sample test code: 87 | ```JS 88 | const SERVICE_UNDER_TEST_PORT = process.env.PORT || 8080; 89 | import { malabi } from 'malabi'; 90 | 91 | import { expect } from 'chai'; 92 | import axios from 'axios'; 93 | 94 | describe('testing service-under-test remotely', () => { 95 | it('successful /todo request', async () => { 96 | // get spans created from the previous call 97 | const telemetryRepo = await malabi(async () => { 98 | await axios(`http://localhost:${SERVICE_UNDER_TEST_PORT}/todo`); 99 | }); 100 | 101 | // Validate internal HTTP call 102 | const todoInternalHTTPCall = telemetryRepo.spans.outgoing().first; 103 | expect(todoInternalHTTPCall.httpFullUrl).equals('https://jsonplaceholder.typicode.com/todos/1') 104 | expect(todoInternalHTTPCall.statusCode).equals(200); 105 | }); 106 | }); 107 | ``` 108 | 109 | Notice the usage of the malabi function - any piece of code that we put inside the callback given to this function would be instrumented as part 110 | of a newly created trace (created by malabi), and the return value would be the telemetry repository for this test, meaning the 111 | Open Telemetry data you can make assertions on (the spans that were created because of the code you put in the callback). 112 | 113 | To sum it up, be sure that whenever you want to make assertions on a span - the code that created it must be in the callback the malabi function receives, and the malabi function returns the spans created. 114 | 115 | ## Storage Backends 116 | Malabi supports 2 types of storage backends for the telemetry data created in your test (spans and traces). 117 | 1. InMemory 118 | In this mode malabi stores the data in memory. 119 | 120 | To select this mode, set MALABI_STORAGE_BACKEND env var to `InMemory` 121 | 2. Jaeger 122 | To select this mode, set MALABI_STORAGE_BACKEND env var to `Jaeger` when running your service under test. 123 | Also, you can control additional env vars here: 124 | 1. OTEL_EXPORTER_JAEGER_AGENT_HOST - lets you control the hostname of the jaeger agent. it must be running somewhere for this mode to work and it's up to you to make it run. default: `localhost` 125 | Example values: `localhost`,`example.com`. 126 | 2. OTEL_EXPORTER_JAEGER_AGENT_PORT - port of jaeger agent. default: `6832` 127 | 3. MALABI_JAEGER_QUERY_PROTOCOL - the protocol used to query jaeger API for the spans. Either `http`(default) or `https`. 128 | 4. MALABI_JAEGER_QUERY_PORT - the port which we use to query jaeger. default: `16686` 129 | 5. MALABI_JAEGER_QUERY_HOST - ets you control the hostname of the jaeger query api. default: `localhost` 130 | 131 | For both storage backends, malabi creates an endpoint (hosted inside the service-under-test) for the test runner to call query. 132 | 133 | ## Caveat: Usage with Jest 134 | 135 | Currently, Jest does not play out well with OpenTelemetry due to Jest's modifications of the way modules are required and OTEL's usage of 136 | require in the middle. 137 | 138 | Until this is fixed, we recommend using Malabi with Mocha instead of Jest. 139 | 140 | ## Documentation 141 | [Click to view documentation](https://aspecto-io.github.io/malabi/index.html) 142 | 143 | ## Why should you care about Malabi 144 | Most distributed apps developers choose to have some kind of black box test (API, integration, end to end, UI, you name it!). 145 | 146 | Black box test create real network activity which is instrumented by OpenTelemetry (which you should have regardless of Malabi). 147 | 148 | Imagine that you can take any existing black box test and validate any backend activity created by it. 149 | 150 | #### Common use case 151 | You are running an API call that create a new DB record, then you write dedicated test code to fetch the record created and validate it. 152 | Now you can rely on Malabi to validate that mongo got the right data with no special code. 153 | 154 | ## Trace based testing explained 155 | Trace-based testing is a method that allows us to improve assertion capabilities by leveraging traces data and make it accessible while setting our expectations from a test. That enables us to **validate essential relationships between software components that otherwise are put to the test only in production**. 156 | Trace-based validation enables developers to become proactive to issues instead of reactive. 157 | ## More examples 158 | 159 | ```JS 160 | import { malabi } from 'malabi'; 161 | 162 | it('should select from db', async () => { 163 | const { spans } = await malabi(async () => { 164 | // some code here that makes db operations with sequelize 165 | }); 166 | 167 | // Validating that /users had ran a single select statement and responded with an array. 168 | const sequelizeActivities = spans.sequelize(); 169 | expect(sequelizeActivities.length).toBe(1); 170 | expect(sequelizeActivities.first.dbOperation).toBe("SELECT"); 171 | expect(Array.isArray(JSON.parse(sequelizeActivities.first.dbResponse))).toBe(true); 172 | }); 173 | ``` 174 | 175 | [See in-repo live example](https://github.com/aspecto-io/malabi/tree/master/examples/README.md) 176 | 177 | ## Project Status 178 | Malabi project is actively maintained by [Aspecto](https://www.aspecto.io), and is currently in it's initial days. We would love to receive your feedback, ideas & contributions in the [discussions](https://github.com/aspecto-io/malabi/discussions) section. 179 | -------------------------------------------------------------------------------- /packages/malabi/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "malabi", 3 | "version": "0.0.7-alpha.1", 4 | "homepage": "https://github.com/aspecto-io/malabi#readme", 5 | "license": "Apache-2.0", 6 | "main": "dist/src/index.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "git+https://github.com/aspecto-io/malabi.git" 10 | }, 11 | "files": [ 12 | "dist/src/**/*.js", 13 | "dist/src/**/*.d.ts", 14 | "LICENSE", 15 | "README.md" 16 | ], 17 | "scripts": { 18 | "build": "tsc", 19 | "watch": "tsc -w" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/aspecto-io/malabi/issues" 23 | }, 24 | "dependencies": { 25 | "@opentelemetry/api": "^1.8.0", 26 | "@opentelemetry/core": "^1.8.0", 27 | "@opentelemetry/exporter-jaeger": "^1.0.1", 28 | "@opentelemetry/instrumentation": "^0.51.1", 29 | "@opentelemetry/sdk-trace-base": "^1.24.1", 30 | "@opentelemetry/sdk-trace-node": "^1.24.1", 31 | "@opentelemetry/semantic-conventions": "^1.24.1", 32 | "axios": "^0.21.1", 33 | "malabi-instrumentation-node": "^0.0.7-alpha.1", 34 | "malabi-telemetry-repository": "^0.0.7-alpha.1", 35 | "opentelemetry-proto-transformations": "^0.0.7-alpha.1", 36 | "opentelemetry-span-transformations": "0.1.0" 37 | }, 38 | "devDependencies": { 39 | "typescript": "^4.2.4" 40 | }, 41 | "gitHead": "0ee1da82de292ead797f53c05591eaadfb9a7948" 42 | } 43 | -------------------------------------------------------------------------------- /packages/malabi/src/exporter/index.ts: -------------------------------------------------------------------------------- 1 | import { ReadableSpan, InMemorySpanExporter } from '@opentelemetry/sdk-trace-base'; 2 | 3 | export const inMemoryExporter = new InMemorySpanExporter(); 4 | 5 | /**** 6 | * Fetches spans from in memory exporter, returns ReadableSpan[] filtered by traceID 7 | * @param traceID 8 | */ 9 | export const getInMemorySpans = ({ traceID }: { traceID: string }): ReadableSpan[] => 10 | inMemoryExporter.getFinishedSpans().filter(span => span.spanContext().traceId === traceID); 11 | 12 | -------------------------------------------------------------------------------- /packages/malabi/src/exporter/jaeger.ts: -------------------------------------------------------------------------------- 1 | import { JaegerExporter } from '@opentelemetry/exporter-jaeger'; 2 | import { convertJaegerSpanToOtelReadableSpan } from 'opentelemetry-span-transformations'; 3 | import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; 4 | 5 | const JAEGER_QUERY_HOST = process.env.MALABI_JAEGER_QUERY_HOST || 'localhost'; 6 | const JAEGER_AGENT_PORT = process.env.OTEL_EXPORTER_JAEGER_AGENT_PORT ? (parseInt(process.env.OTEL_EXPORTER_JAEGER_AGENT_PORT)) : 6832; 7 | export const jaegerExporter = new JaegerExporter({ 8 | tags: [], 9 | port: JAEGER_AGENT_PORT 10 | }); 11 | 12 | /**** 13 | * Fetches spans from jaeger internal JSON API by service, returns ReadableSpan[] filtered by traceID 14 | * @param serviceName 15 | * @param traceID 16 | */ 17 | export const getJaegerSpans = async ({ 18 | serviceName, 19 | traceID, 20 | }: { serviceName: string, traceID: string }): Promise => { 21 | const JAEGER_QUERY_PROTOCOL = process.env.MALABI_JAEGER_QUERY_PROTOCOL || 'http'; 22 | const JAEGER_QUERY_PORT = process.env.MALABI_JAEGER_QUERY_PORT || '16686'; 23 | const axios = require('axios'); 24 | const res = await axios.get(`${JAEGER_QUERY_PROTOCOL}://${JAEGER_QUERY_HOST}:${JAEGER_QUERY_PORT}/api/traces?service=${serviceName}`) 25 | const spansInJaegerFormat = res.data.data.find(({ traceID: id }) => id === traceID).spans; 26 | return spansInJaegerFormat.map(jaegerSpan => convertJaegerSpanToOtelReadableSpan(jaegerSpan)); 27 | } 28 | 29 | -------------------------------------------------------------------------------- /packages/malabi/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './instrumentation'; 2 | export * from './remote-runner-integration'; 3 | export * from 'malabi-telemetry-repository'; 4 | -------------------------------------------------------------------------------- /packages/malabi/src/instrumentation/index.ts: -------------------------------------------------------------------------------- 1 | import { getNodeAutoInstrumentations } from 'malabi-instrumentation-node'; 2 | 3 | import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node'; 4 | import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'; 5 | import { ParentBasedSampler } from '@opentelemetry/core'; 6 | import { registerInstrumentations } from '@opentelemetry/instrumentation'; 7 | import { inMemoryExporter } from '../exporter'; 8 | import { jaegerExporter } from '../exporter/jaeger'; 9 | import { 10 | Context, 11 | Link, 12 | Sampler, 13 | SamplingDecision, 14 | SamplingResult, 15 | SpanAttributes, 16 | SpanKind, 17 | trace, 18 | } from '@opentelemetry/api'; 19 | import { SemanticAttributes, SemanticResourceAttributes } from '@opentelemetry/semantic-conventions'; 20 | import { Resource } from '@opentelemetry/resources'; 21 | import { fetchRemoteTelemetry } from '../remote-runner-integration'; 22 | import { TelemetryRepository } from 'malabi-telemetry-repository'; 23 | 24 | // allow all but filter out malabi requests. 25 | class MalabiSampler implements Sampler { 26 | shouldSample( 27 | _context: Context, 28 | _traceId: string, 29 | _spanName: string, 30 | _spanKind: SpanKind, 31 | attributes: SpanAttributes, 32 | _links: Link[] 33 | ): SamplingResult { 34 | const httpTarget = attributes[SemanticAttributes.HTTP_TARGET] as string; 35 | return { 36 | decision: httpTarget?.startsWith('/malabi') 37 | ? SamplingDecision.NOT_RECORD 38 | : SamplingDecision.RECORD_AND_SAMPLED, 39 | }; 40 | } 41 | 42 | toString(): string { 43 | return 'malabi sampler'; 44 | } 45 | } 46 | 47 | export enum StorageBackend { 48 | InMemory = 'InMemory', 49 | Jaeger = 'Jaeger', 50 | } 51 | 52 | const STORAGE_BACKEND_TO_EXPORTER = { 53 | [StorageBackend.InMemory]: inMemoryExporter, 54 | [StorageBackend.Jaeger]: jaegerExporter, 55 | }; 56 | 57 | export interface InstrumentationConfig { 58 | serviceName: string; 59 | } 60 | 61 | const verifyStorageBackendEnvVar = () => { 62 | const storageBackendNames = Object.keys(STORAGE_BACKEND_TO_EXPORTER); 63 | 64 | // If MALABI_STORAGE_BACKEND is defined it must be one of the supported values. if not - throw. 65 | // If MALABI_STORAGE_BACKEND is undefined - default to in memory. 66 | if (process.env.MALABI_STORAGE_BACKEND && ( 67 | !storageBackendNames.includes(process.env.MALABI_STORAGE_BACKEND))) { 68 | throw new Error(`Unsupported malabi storage backend. must be either ${JSON.stringify(storageBackendNames)}`); 69 | } 70 | } 71 | 72 | /** 73 | * Enables OpenTelemetry instrumentation for Malabi. Used in both test runner and service under test 74 | * @category Main Functions 75 | * @param InstrumentationConfig Config for creating the instrumentation 76 | * @param InstrumentationConfig.serviceName The name of the tested service 77 | */ 78 | export const instrument = ({ 79 | serviceName 80 | }: InstrumentationConfig) => { 81 | verifyStorageBackendEnvVar(); 82 | 83 | const tracerProvider = new NodeTracerProvider({ 84 | resource: new Resource({ 85 | [SemanticResourceAttributes.SERVICE_NAME]: serviceName, 86 | }), 87 | sampler: new ParentBasedSampler({ root: new MalabiSampler() }), 88 | }); 89 | 90 | const exporter = STORAGE_BACKEND_TO_EXPORTER[process.env.MALABI_STORAGE_BACKEND || StorageBackend.InMemory]; 91 | tracerProvider.addSpanProcessor(new SimpleSpanProcessor(exporter)); 92 | tracerProvider.register(); 93 | 94 | registerInstrumentations({ 95 | instrumentations: getNodeAutoInstrumentations({ 96 | collectPayloads: true, 97 | suppressInternalInstrumentation: true, 98 | }), 99 | tracerProvider, 100 | }); 101 | }; 102 | 103 | /** 104 | * A wrapper that handles creating a span per test run. returns the spans that were created inside the callback function ready for assertion. 105 | * @category Main Functions 106 | * @param callback an async function containing all of the current test's span generating operations(API calls etc) 107 | */ 108 | export const malabi = async (callback): Promise => { 109 | return new Promise((resolve, reject) => { 110 | if (!process.env.MALABI_ENDPOINT_PORT_OR_URL) reject('MALABI_ENDPOINT_PORT_OR_URL was not found. This is the port/url at which the service under test is hosting malabi endpoint'); 111 | // TODO use current package name and version from package.json 112 | const tracer = trace.getTracer('malabiManualTracer'); 113 | tracer.startActiveSpan('malabiRoot', async (span) => { 114 | const currTraceID = span.spanContext().traceId; 115 | try { 116 | await callback(); 117 | } 118 | catch (ex) { 119 | console.error('Error caught inside malabi callback', ex); 120 | reject(ex); 121 | } 122 | span.end(); 123 | const telemetry = await fetchRemoteTelemetry({ 124 | portOrBaseUrl: process.env.MALABI_ENDPOINT_PORT_OR_URL, 125 | currentTestTraceID: currTraceID, 126 | }); 127 | resolve(telemetry); 128 | }); 129 | }) 130 | } 131 | 132 | -------------------------------------------------------------------------------- /packages/malabi/src/remote-runner-integration/fetch-remote-telemetry.ts: -------------------------------------------------------------------------------- 1 | import { collectorTraceV1Transform } from 'opentelemetry-proto-transformations'; 2 | import { initRepository, TelemetryRepository } from 'malabi-telemetry-repository'; 3 | 4 | interface FetchRemoteTelemetryProps { 5 | portOrBaseUrl: string | number; 6 | currentTestTraceID: string; 7 | } 8 | 9 | /** 10 | * Fetches the spans from the exposed malabi spans endpoint 11 | * @category Main Functions 12 | * @internal 13 | * @param fetchRemoteTelemetryProps Props for fetching remote telemetry 14 | * @param fetchRemoteTelemetryProps.portOrBaseUrl port number, or entire base url, where the endpoint is hosted at. 15 | */ 16 | const fetchRemoteTelemetry = async ({ portOrBaseUrl, currentTestTraceID } : FetchRemoteTelemetryProps): Promise => { 17 | try { 18 | const baseUrl = portOrBaseUrl.toString().startsWith('http') ? portOrBaseUrl : `http://localhost:${portOrBaseUrl}`; 19 | const axios = require('axios'); 20 | const res = await axios.get(`${baseUrl}/malabi/spans`, { 21 | transformResponse: (res: any) => { 22 | return res; 23 | }, 24 | params: { 25 | traceID: currentTestTraceID, 26 | } 27 | }); 28 | 29 | const protoFormatted = collectorTraceV1Transform.fromJsonEncodedProtobufFormat(res.data); 30 | const spans = collectorTraceV1Transform.fromProtoExportTraceServiceRequest(protoFormatted); 31 | 32 | return initRepository(spans); 33 | } catch (err) { 34 | console.error('error while fetching remote telemetry', err); 35 | } 36 | return initRepository([]); 37 | }; 38 | 39 | export default fetchRemoteTelemetry; 40 | -------------------------------------------------------------------------------- /packages/malabi/src/remote-runner-integration/http-server.ts: -------------------------------------------------------------------------------- 1 | import { getJaegerSpans } from '../exporter/jaeger'; 2 | import { InstrumentationConfig, StorageBackend } from '../instrumentation'; 3 | import { getInMemorySpans } from '../exporter'; 4 | import { collectorTraceV1Transform } from 'opentelemetry-proto-transformations'; 5 | 6 | export const getMalabiExpressRouter = ({ serviceName }: InstrumentationConfig) => { 7 | const express = require('express'); 8 | return express 9 | .Router() 10 | .get('/spans', async (_req, res) => { 11 | const { query: { traceID } } = _req; 12 | if (!traceID) res.status(400).json({ message: 'Must provide a valid traceID' }); 13 | res.set('Content-Type', 'application/json'); 14 | const spans = collectorTraceV1Transform.toJsonEncodedProtobufFormat( 15 | collectorTraceV1Transform.toProtoExportTraceServiceRequest(process.env.MALABI_STORAGE_BACKEND === StorageBackend.Jaeger ? 16 | await getJaegerSpans({ serviceName, traceID }) : getInMemorySpans({ traceID }))); 17 | res.send(spans); 18 | }) 19 | }; 20 | 21 | /*** 22 | * Exposes an endpoint that returns spans created during the test run 23 | * @param port the port on which to expose the malabi endpoint 24 | * @param instrumentationConfig contains the service name being tested. example: { serviceName: 'some-service' } 25 | */ 26 | export const serveMalabiFromHttpApp = (port: number, instrumentationConfig: InstrumentationConfig) => { 27 | const express = require('express'); 28 | const app = express(); 29 | app.use('/malabi', getMalabiExpressRouter(instrumentationConfig)); 30 | app.listen(port); 31 | return app; 32 | }; 33 | -------------------------------------------------------------------------------- /packages/malabi/src/remote-runner-integration/index.ts: -------------------------------------------------------------------------------- 1 | import fetchRemoteTelemetry from './fetch-remote-telemetry'; 2 | 3 | export { serveMalabiFromHttpApp, getMalabiExpressRouter } from './http-server'; 4 | export { fetchRemoteTelemetry }; 5 | -------------------------------------------------------------------------------- /packages/malabi/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../packages/tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "./dist" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/opentelemetry-instrumentation-mocha/README.md: -------------------------------------------------------------------------------- 1 | # OpenTelemetry mocha Instrumentation for Node.js 2 | [![NPM version](https://img.shields.io/npm/v/opentelemetry-instrumentation-mocha.svg)](https://www.npmjs.com/package/opentelemetry-instrumentation-mocha) 3 | 4 | This module is a mocha [root hook plugin](https://mochajs.org/#root-hook-plugins) and [reporter](https://mochajs.org/#third-party-reporters) which, in the presence of an installed open-telemetry SDK, provides automatic instrumentation for the [mocha testing framework](https://mochajs.org/). 5 | 6 | ## Installation 7 | 8 | ``` 9 | npm install --save opentelemetry-instrumentation-mocha 10 | ``` 11 | 12 | ## Supported Versions 13 | This instrumentation uses [root hook plugins](https://mochajs.org/#root-hook-plugins) which are available from mocha ^8.0.0 14 | 15 | ## Usage 16 | For opentelemetry integration to work, you need to configure your mocha run to use otel-plugin and otel-reporter. 17 | 18 | - otel-plugin - takes care of setting the otel context during the execution of the test, which create nested spans in the same trace as the test. This is done via the `--require` option for mocha. 19 | - otel-reporter - creates a span for each test, add relevant attributes and status. Reporter is registered via the `--reporter` option for mocha. Since there can only be one reporter, you might want to use [`mocha-multi-reporters`](https://www.npmjs.com/package/mocha-multi-reporters) so you'll get the run report as you're used to. 20 | 21 | ### CLI 22 | ```js 23 | mocha --require ./node_modules/opentelemetry-instrumentation-mocha/dist/src/otel-plugin.js --reporter ./node_modules/opentelemetry-instrumentation-mocha/dist/src/otel-reporter.js 24 | ``` 25 | 26 | ### package.json 27 | ```json 28 | "mocha": { 29 | "require": "opentelemetry-instrumentation-mocha", 30 | "reporter": "./node_modules/opentelemetry-instrumentation-mocha/dist/src/otel-reporter.js" 31 | } 32 | ``` 33 | 34 | If you already `require`ing a plugin, you can use JSON array to use multi plugins: 35 | ```json 36 | "mocha": { 37 | "require": ["some-other-plugin", "opentelemetry-instrumentation-mocha"], 38 | "reporter": "./node_modules/opentelemetry-instrumentation-mocha/dist/src/otel-reporter.js" 39 | } 40 | ``` 41 | 42 | For reporter, it is not possible to specify multiple values. Setting otel-reporter will override the default reporter which means you will not get test run reports to you console (or any other report you are using). 43 | You can use [mocha-multi-reporters](https://www.npmjs.com/package/mocha-multi-reporters) for that. 44 | -------------------------------------------------------------------------------- /packages/opentelemetry-instrumentation-mocha/multi-reporters-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "reporterEnabled": "spec, ./dist/src/otel-reporter.js" 3 | } 4 | -------------------------------------------------------------------------------- /packages/opentelemetry-instrumentation-mocha/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opentelemetry-instrumentation-mocha", 3 | "version": "0.0.7-alpha.1", 4 | "description": "mocha root hook plugin for open-telemetry instrumentation", 5 | "keywords": [ 6 | "mocha", 7 | "opentelemetry" 8 | ], 9 | "homepage": "https://github.com/aspecto-io/malabi", 10 | "license": "Apache-2.0", 11 | "main": "dist/src/index.js", 12 | "files": [ 13 | "dist/src/**/*.js", 14 | "dist/src/**/*.d.ts", 15 | "LICENSE", 16 | "README.md" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/aspecto-io/malabi.git" 21 | }, 22 | "scripts": { 23 | "build": "tsc", 24 | "prepare": "yarn run build", 25 | "test": "mocha --reporter mocha-multi-reporters --reporter-options configFile=multi-reporters-config.json", 26 | "watch": "tsc -w", 27 | "version:update": "node ../../scripts/version-update.js", 28 | "version": "yarn run version:update", 29 | "test-all-versions": "tav", 30 | "test:ci": "yarn test-all-versions" 31 | }, 32 | "bugs": { 33 | "url": "https://github.com/aspecto-io/opentelemetry-ext-js/issues" 34 | }, 35 | "peerDependencies": { 36 | "@opentelemetry/api": "^1.24.1" 37 | }, 38 | "dependencies": { 39 | "@opentelemetry/instrumentation": "^0.51.1" 40 | }, 41 | "devDependencies": { 42 | "@opentelemetry/api": "^1.8.0", 43 | "@opentelemetry/sdk-trace-base": "^1.24.1", 44 | "@opentelemetry/sdk-trace-node": "^1.24.1", 45 | "@types/mocha": "^8.2.2", 46 | "expect": "^26.6.2", 47 | "mocha-multi-reporters": "^1.5.1", 48 | "ts-mocha": "^8.0.0" 49 | }, 50 | "mocha": { 51 | "extension": [ 52 | "ts" 53 | ], 54 | "spec": "test/**/*.spec.ts", 55 | "reporter": "mocha-multi-reporters", 56 | "require": [ 57 | "./test/instrument.js", 58 | "./dist/src/index.js", 59 | "ts-node/register" 60 | ] 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /packages/opentelemetry-instrumentation-mocha/src/index.ts: -------------------------------------------------------------------------------- 1 | export { mochaHooks } from './otel-plugin'; 2 | -------------------------------------------------------------------------------- /packages/opentelemetry-instrumentation-mocha/src/instrumentation.ts: -------------------------------------------------------------------------------- 1 | import { trace, Span, SpanStatusCode, SpanKind } from '@opentelemetry/api'; 2 | import type { Runnable, Suite } from 'mocha'; 3 | import { TestAttributes } from './types'; 4 | import { VERSION } from './version'; 5 | 6 | export const TEST_SPAN_KEY = Symbol.for('opentelemetry.mocha.span_key'); 7 | 8 | const instrumentationLibraryName = 'opentelemetry-instrumentation-mocha'; 9 | 10 | type TestWithSpan = { 11 | [TEST_SPAN_KEY]?: Span; 12 | } & Runnable; 13 | 14 | const getSuitesRecursive = (suite: Suite): string[] => { 15 | if (!suite) { 16 | return []; 17 | } 18 | 19 | const parentSuites = getSuitesRecursive(suite.parent); 20 | return suite.title ? [...parentSuites, suite.title] : parentSuites; 21 | }; 22 | 23 | const getFullName = (suites: string[], testName: string): string => `${[...suites, testName].join(' -> ')}`; 24 | 25 | export const startSpan = (test: TestWithSpan): Span => { 26 | const existingSpan = getTestSpan(test); 27 | if (existingSpan) { 28 | return existingSpan; 29 | } 30 | 31 | const tracer = trace.getTracer(instrumentationLibraryName, VERSION); 32 | 33 | const testName = test.title; 34 | const suites = getSuitesRecursive(test.parent); 35 | const fullName = getFullName(suites, testName); 36 | 37 | const attributes = { 38 | [TestAttributes.TEST_FRAMEWORK]: 'mocha', 39 | [TestAttributes.TEST_NAME]: testName, 40 | [TestAttributes.TEST_FULL_NAME]: fullName, 41 | [TestAttributes.TEST_SUITES]: suites, 42 | }; 43 | 44 | const retries = (test as any).retries(); 45 | if (retries >= 0) { 46 | attributes[TestAttributes.TEST_RETRIES] = retries; 47 | 48 | const currentRetry = (test as any).currentRetry(); 49 | if (currentRetry != null) { 50 | attributes[TestAttributes.TEST_CURRENT_RETRY] = currentRetry; 51 | } 52 | } 53 | 54 | const spanForTest = tracer.startSpan(test.fullTitle(), { 55 | attributes, 56 | root: true, 57 | kind: SpanKind.CLIENT, 58 | }); 59 | Object.defineProperty(test, TEST_SPAN_KEY, { 60 | value: spanForTest, 61 | enumerable: false, 62 | // configurable: false, 63 | }); 64 | 65 | return spanForTest; 66 | }; 67 | 68 | export const endSpan = (test: TestWithSpan, err: any) => { 69 | const spanForTest: Span = getTestSpan(test); 70 | if (!spanForTest) { 71 | return; 72 | } 73 | 74 | if (!spanForTest.isRecording()) { 75 | return; 76 | } 77 | 78 | if (err) { 79 | spanForTest.recordException(err); 80 | spanForTest.setStatus({ 81 | code: SpanStatusCode.ERROR, 82 | message: err.message, 83 | }); 84 | } 85 | 86 | spanForTest.setAttribute(TestAttributes.TEST_RESULT_TIMEDOUT, test.timedOut); 87 | spanForTest.end(); 88 | }; 89 | 90 | export const getTestSpan = (test: TestWithSpan): Span => { 91 | return test[TEST_SPAN_KEY]; 92 | }; 93 | -------------------------------------------------------------------------------- /packages/opentelemetry-instrumentation-mocha/src/otel-plugin.ts: -------------------------------------------------------------------------------- 1 | import { context, trace } from '@opentelemetry/api'; 2 | import type { Runnable } from 'mocha'; 3 | import { endSpan, startSpan } from './instrumentation'; 4 | 5 | export const mochaHooks = { 6 | beforeEach(done) { 7 | const test = this.currentTest as Runnable; 8 | const spanForTest = startSpan(test); 9 | context.with(trace.setSpan(context.active(), spanForTest), done); 10 | }, 11 | 12 | afterEach(done) { 13 | const test = this.currentTest as Runnable; 14 | endSpan(test, (test as any).err); 15 | done(); 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /packages/opentelemetry-instrumentation-mocha/src/otel-reporter.js: -------------------------------------------------------------------------------- 1 | var mocha = require('mocha'); 2 | var { endSpan, startSpan } = require('./instrumentation'); 3 | module.exports = OtelReporter; 4 | 5 | function OtelReporter(runner) { 6 | mocha.reporters.Base.call(this, runner); 7 | 8 | runner.on('test', function (test) { 9 | startSpan(test); 10 | }); 11 | 12 | runner.on('retry', function (test, err) { 13 | endSpan(test, err); 14 | }); 15 | 16 | runner.on('pass', function (test) { 17 | endSpan(test); 18 | }); 19 | 20 | runner.on('fail', function (test, err) { 21 | endSpan(test, err); 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /packages/opentelemetry-instrumentation-mocha/src/types.ts: -------------------------------------------------------------------------------- 1 | import { InstrumentationConfig } from '@opentelemetry/instrumentation'; 2 | 3 | export enum TestAttributes { 4 | /** Name of the testing framework executing the test */ 5 | TEST_FRAMEWORK = 'test.framework', 6 | 7 | /** Name of the test itself, without suites */ 8 | TEST_NAME = 'test.name', 9 | 10 | /** Name of the test with the suites */ 11 | TEST_FULL_NAME = 'test.full_name', 12 | 13 | /** Array of suites in which the test reside, in order of nesting (from outer to inner) */ 14 | TEST_SUITES = 'test.suites', 15 | 16 | /** boolean value indicating it the test failed due to timeout */ 17 | TEST_RESULT_TIMEDOUT = 'test.result.timedout', 18 | 19 | /** How many timed to retry failed test */ 20 | TEST_RETRIES = 'test.retries', 21 | 22 | /** the retry attempt to run this test */ 23 | TEST_CURRENT_RETRY = 'test.current_retry', 24 | } 25 | 26 | export interface MochaInstrumentationConfig extends InstrumentationConfig {} 27 | -------------------------------------------------------------------------------- /packages/opentelemetry-instrumentation-mocha/src/version.ts: -------------------------------------------------------------------------------- 1 | // this is autogenerated file, see scripts/version-update.js 2 | export const VERSION = '0.0.7-alpha.1'; 3 | -------------------------------------------------------------------------------- /packages/opentelemetry-instrumentation-mocha/test/instrument.js: -------------------------------------------------------------------------------- 1 | const { SimpleSpanProcessor, InMemorySpanExporter } = require('@opentelemetry/sdk-trace-base'); 2 | const { NodeTracerProvider } = require('@opentelemetry/sdk-trace-node'); 3 | 4 | const provider = new NodeTracerProvider(); 5 | 6 | const memoryExporter = new InMemorySpanExporter(); 7 | provider.addSpanProcessor(new SimpleSpanProcessor(memoryExporter)); 8 | provider.register(); 9 | 10 | module.exports = memoryExporter; 11 | -------------------------------------------------------------------------------- /packages/opentelemetry-instrumentation-mocha/test/mocha.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import expect from 'expect'; 3 | import type { InMemorySpanExporter } from '@opentelemetry/sdk-trace-base'; 4 | const memoryExporter: InMemorySpanExporter = require('./instrument'); 5 | import { TestAttributes } from '../src/types'; 6 | import { SpanKind, SpanStatusCode, trace } from '@opentelemetry/api'; 7 | 8 | describe('mocha', () => { 9 | describe('empty test', () => { 10 | before(() => { 11 | memoryExporter.reset(); 12 | }); 13 | 14 | it('successful test', () => { 15 | // this test just validate that a span is created 16 | }); 17 | 18 | it('validate successful test', () => { 19 | expect(memoryExporter.getFinishedSpans().length).toBe(1); 20 | const [testSpan] = memoryExporter.getFinishedSpans(); 21 | 22 | // name 23 | expect(testSpan.name).toMatch('successful test'); 24 | 25 | // attributes 26 | expect(testSpan.attributes[TestAttributes.TEST_NAME]).toMatch('successful test'); 27 | expect(testSpan.attributes[TestAttributes.TEST_FULL_NAME]).toMatch('successful test'); 28 | expect(testSpan.attributes[TestAttributes.TEST_SUITES]).toStrictEqual(['mocha', 'empty test']); 29 | expect(testSpan.attributes[TestAttributes.TEST_RESULT_TIMEDOUT]).toBe(false); 30 | expect(testSpan.attributes[TestAttributes.TEST_RETRIES]).toBeUndefined(); 31 | 32 | // status 33 | expect(testSpan.status.code).toBe(SpanStatusCode.UNSET); 34 | expect(testSpan.status.message).toBeUndefined(); 35 | 36 | // kind 37 | expect(testSpan.kind).toBe(SpanKind.CLIENT); 38 | }); 39 | }); 40 | 41 | describe('retried test', function () { 42 | this.retries(2); 43 | let retryCount = 0; 44 | 45 | before(() => { 46 | memoryExporter.reset(); 47 | }); 48 | 49 | it('retry test', () => { 50 | // fail just the first retry so it won't fail the entire run 51 | if (retryCount === 0) { 52 | retryCount++; 53 | expect(true).toBeFalsy(); 54 | } 55 | }); 56 | 57 | it('validate retry test', () => { 58 | if (retryCount !== 1) { 59 | return; 60 | } 61 | expect(memoryExporter.getFinishedSpans().length).toBe(2); 62 | const [firstRun, retry] = memoryExporter.getFinishedSpans(); 63 | 64 | expect(firstRun.attributes[TestAttributes.TEST_RETRIES]).toBe(2); 65 | expect(firstRun.attributes[TestAttributes.TEST_CURRENT_RETRY]).toBe(0); 66 | expect(firstRun.status.code).toBe(SpanStatusCode.ERROR); 67 | expect(firstRun.status.message).not.toBeUndefined(); 68 | 69 | expect(retry.attributes[TestAttributes.TEST_RETRIES]).toBe(2); 70 | expect(retry.attributes[TestAttributes.TEST_CURRENT_RETRY]).toBe(1); 71 | expect(retry.status.code).toBe(SpanStatusCode.UNSET); 72 | expect(retry.status.message).toBeUndefined(); 73 | }); 74 | }); 75 | 76 | describe('span created in test', () => { 77 | before(() => { 78 | memoryExporter.reset(); 79 | }); 80 | 81 | it('test with span', () => { 82 | const tracer = trace.getTracer('tracer for unittest'); 83 | tracer.startSpan('internal span in test').end(); 84 | }); 85 | 86 | it('validate test with span', () => { 87 | expect(memoryExporter.getFinishedSpans().length).toBe(2); 88 | const [internalSpan, testSpan] = memoryExporter.getFinishedSpans(); 89 | 90 | // make sure that internal span is the child of test span 91 | expect(internalSpan.parentSpanId).toEqual(testSpan.spanContext().spanId); 92 | }); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /packages/opentelemetry-instrumentation-mocha/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist", 6 | "allowJs": true 7 | }, 8 | "include": ["src/**/*.ts", "test/**/*.ts", "src/otel-reporter.js"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/opentelemetry-proto-transformations/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "opentelemetry-proto-transformations", 3 | "version": "0.0.7-alpha.1", 4 | "main": "dist/src/index.js", 5 | "license": "Apache-2.0", 6 | "scripts": { 7 | "build": "tsc", 8 | "watch": "tsc -w" 9 | }, 10 | "files": [ 11 | "dist/src/**/*.js", 12 | "dist/src/**/*.d.ts", 13 | "LICENSE", 14 | "README.md" 15 | ], 16 | "dependencies": { 17 | "@opentelemetry/api": "^1.8.0", 18 | "@opentelemetry/core": "^1.8.0", 19 | "@opentelemetry/resources": "^1.24.1", 20 | "@opentelemetry/sdk-trace-base": "^1.24.1", 21 | "ts-proto": "^1.79.7" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/opentelemetry-proto-transformations/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as collectorTraceV1Transform from './opentelemetry/proto/collector/trace/v1/transform'; 2 | -------------------------------------------------------------------------------- /packages/opentelemetry-proto-transformations/src/opentelemetry/proto/collector/trace/v1/trace_service.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import Long from 'long'; 3 | import _m0 from 'protobufjs/minimal'; 4 | import { ResourceSpans } from '../../../../../opentelemetry/proto/trace/v1/trace'; 5 | 6 | export const protobufPackage = 'opentelemetry.proto.collector.trace.v1'; 7 | 8 | export interface ExportTraceServiceRequest { 9 | /** 10 | * An array of ResourceSpans. 11 | * For data coming from a single resource this array will typically contain one 12 | * element. Intermediary nodes (such as OpenTelemetry Collector) that receive 13 | * data from multiple origins typically batch the data before forwarding further and 14 | * in that case this array will contain multiple elements. 15 | */ 16 | resourceSpans: ResourceSpans[]; 17 | } 18 | 19 | export interface ExportTraceServiceResponse {} 20 | 21 | const baseExportTraceServiceRequest: object = {}; 22 | 23 | export const ExportTraceServiceRequest = { 24 | encode(message: ExportTraceServiceRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { 25 | for (const v of message.resourceSpans) { 26 | ResourceSpans.encode(v!, writer.uint32(10).fork()).ldelim(); 27 | } 28 | return writer; 29 | }, 30 | 31 | decode(input: _m0.Reader | Uint8Array, length?: number): ExportTraceServiceRequest { 32 | const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input); 33 | let end = length === undefined ? reader.len : reader.pos + length; 34 | const message = { 35 | ...baseExportTraceServiceRequest, 36 | } as ExportTraceServiceRequest; 37 | message.resourceSpans = []; 38 | while (reader.pos < end) { 39 | const tag = reader.uint32(); 40 | switch (tag >>> 3) { 41 | case 1: 42 | message.resourceSpans.push(ResourceSpans.decode(reader, reader.uint32())); 43 | break; 44 | default: 45 | reader.skipType(tag & 7); 46 | break; 47 | } 48 | } 49 | return message; 50 | }, 51 | 52 | fromJSON(object: any): ExportTraceServiceRequest { 53 | const message = { 54 | ...baseExportTraceServiceRequest, 55 | } as ExportTraceServiceRequest; 56 | message.resourceSpans = []; 57 | if (object.resourceSpans !== undefined && object.resourceSpans !== null) { 58 | for (const e of object.resourceSpans) { 59 | message.resourceSpans.push(ResourceSpans.fromJSON(e)); 60 | } 61 | } 62 | return message; 63 | }, 64 | 65 | toJSON(message: ExportTraceServiceRequest): unknown { 66 | const obj: any = {}; 67 | if (message.resourceSpans) { 68 | obj.resourceSpans = message.resourceSpans.map((e) => (e ? ResourceSpans.toJSON(e) : undefined)); 69 | } else { 70 | obj.resourceSpans = []; 71 | } 72 | return obj; 73 | }, 74 | 75 | fromPartial(object: DeepPartial): ExportTraceServiceRequest { 76 | const message = { 77 | ...baseExportTraceServiceRequest, 78 | } as ExportTraceServiceRequest; 79 | message.resourceSpans = []; 80 | if (object.resourceSpans !== undefined && object.resourceSpans !== null) { 81 | for (const e of object.resourceSpans) { 82 | message.resourceSpans.push(ResourceSpans.fromPartial(e)); 83 | } 84 | } 85 | return message; 86 | }, 87 | }; 88 | 89 | const baseExportTraceServiceResponse: object = {}; 90 | 91 | export const ExportTraceServiceResponse = { 92 | encode(_: ExportTraceServiceResponse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { 93 | return writer; 94 | }, 95 | 96 | decode(input: _m0.Reader | Uint8Array, length?: number): ExportTraceServiceResponse { 97 | const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input); 98 | let end = length === undefined ? reader.len : reader.pos + length; 99 | const message = { 100 | ...baseExportTraceServiceResponse, 101 | } as ExportTraceServiceResponse; 102 | while (reader.pos < end) { 103 | const tag = reader.uint32(); 104 | switch (tag >>> 3) { 105 | default: 106 | reader.skipType(tag & 7); 107 | break; 108 | } 109 | } 110 | return message; 111 | }, 112 | 113 | fromJSON(_: any): ExportTraceServiceResponse { 114 | const message = { 115 | ...baseExportTraceServiceResponse, 116 | } as ExportTraceServiceResponse; 117 | return message; 118 | }, 119 | 120 | toJSON(_: ExportTraceServiceResponse): unknown { 121 | const obj: any = {}; 122 | return obj; 123 | }, 124 | 125 | fromPartial(_: DeepPartial): ExportTraceServiceResponse { 126 | const message = { 127 | ...baseExportTraceServiceResponse, 128 | } as ExportTraceServiceResponse; 129 | return message; 130 | }, 131 | }; 132 | 133 | /** 134 | * Service that can be used to push spans between one Application instrumented with 135 | * OpenTelemetry and an collector, or between an collector and a central collector (in this 136 | * case spans are sent/received to/from multiple Applications). 137 | */ 138 | export interface TraceService { 139 | /** 140 | * For performance reasons, it is recommended to keep this RPC 141 | * alive for the entire life of the application. 142 | */ 143 | Export(request: ExportTraceServiceRequest): Promise; 144 | } 145 | 146 | export class TraceServiceClientImpl implements TraceService { 147 | private readonly rpc: Rpc; 148 | constructor(rpc: Rpc) { 149 | this.rpc = rpc; 150 | } 151 | Export(request: ExportTraceServiceRequest): Promise { 152 | const data = ExportTraceServiceRequest.encode(request).finish(); 153 | const promise = this.rpc.request('opentelemetry.proto.collector.trace.v1.TraceService', 'Export', data); 154 | return promise.then((data) => ExportTraceServiceResponse.decode(new _m0.Reader(data))); 155 | } 156 | } 157 | 158 | interface Rpc { 159 | request(service: string, method: string, data: Uint8Array): Promise; 160 | } 161 | 162 | type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; 163 | export type DeepPartial = T extends Builtin 164 | ? T 165 | : T extends Array 166 | ? Array> 167 | : T extends ReadonlyArray 168 | ? ReadonlyArray> 169 | : T extends {} 170 | ? { [K in keyof T]?: DeepPartial } 171 | : Partial; 172 | 173 | if (_m0.util.Long !== Long) { 174 | _m0.util.Long = Long as any; 175 | _m0.configure(); 176 | } 177 | -------------------------------------------------------------------------------- /packages/opentelemetry-proto-transformations/src/opentelemetry/proto/collector/trace/v1/transform.ts: -------------------------------------------------------------------------------- 1 | import { fromProtoResourceSpansArray, toProtoResourceSpansArray } from '../../../trace/v1/transform'; 2 | import * as tracing from '@opentelemetry/sdk-trace-base'; 3 | import * as resources from '@opentelemetry/resources'; 4 | import * as proto from './trace_service'; 5 | import { bytesArrayToHex, hexToBytesArray } from '../../../../../utils'; 6 | 7 | export function toProtoExportTraceServiceRequest( 8 | sdkSpans: tracing.ReadableSpan[], 9 | additionalAttributes: resources.ResourceAttributes = {} 10 | ): proto.ExportTraceServiceRequest { 11 | return { 12 | resourceSpans: toProtoResourceSpansArray(sdkSpans, additionalAttributes), 13 | }; 14 | } 15 | 16 | export function fromProtoExportTraceServiceRequest( 17 | protoExportTraceServiceRequest: proto.ExportTraceServiceRequest 18 | ): tracing.ReadableSpan[] { 19 | return fromProtoResourceSpansArray(protoExportTraceServiceRequest.resourceSpans); 20 | } 21 | 22 | /** 23 | * Serialize the ExportTraceServiceRequest message into Json-Encoded Protobuf format. 24 | * 25 | * From the spec: 26 | * JSON-encoded Protobuf payloads use proto3 standard defined JSON Mapping for mapping 27 | * between Protobuf and JSON, with one deviation from that mapping: the trace_id and 28 | * span_id byte arrays are represented as case-insensitive hex-encoded strings, 29 | * they are not base64-encoded like it is defined in the standard JSON Mapping. 30 | * The hex encoding is used for trace_id and span_id fields in all OTLP Protobuf messages, 31 | * e.g. the Span, Link, LogRecord, etc. messages. 32 | * 33 | * This function replaces the trace and span ids to be hex-encoded string, and return the 34 | * JSON.serialize payload for the request 35 | * 36 | * Spec for reference: 37 | * https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/protocol/otlp.md#otlphttp 38 | */ 39 | export function toJsonEncodedProtobufFormat(exportTraceServiceRequest: proto.ExportTraceServiceRequest): string { 40 | const withHexStringIds = { 41 | resourceSpans: exportTraceServiceRequest.resourceSpans.map((resourceSpan) => ({ 42 | ...resourceSpan, 43 | instrumentationLibrarySpans: resourceSpan.instrumentationLibrarySpans.map( 44 | (instrumentationLibrarySpans) => ({ 45 | ...instrumentationLibrarySpans, 46 | spans: instrumentationLibrarySpans.spans.map((span) => ({ 47 | ...span, 48 | traceId: bytesArrayToHex(span.traceId), 49 | spanId: bytesArrayToHex(span.spanId), 50 | parentSpanId: span.parentSpanId ? bytesArrayToHex(span.parentSpanId) : span.parentSpanId, 51 | links: span.links.map((link) => ({ 52 | ...link, 53 | traceId: bytesArrayToHex(link.traceId), 54 | spanId: bytesArrayToHex(link.spanId), 55 | })), 56 | })), 57 | }) 58 | ), 59 | })), 60 | }; 61 | return JSON.stringify(withHexStringIds); 62 | } 63 | 64 | export function fromJsonEncodedProtobufFormat(jsonStringifyPayload: string): proto.ExportTraceServiceRequest { 65 | const jsonPayload = JSON.parse(jsonStringifyPayload); 66 | return { 67 | resourceSpans: jsonPayload.resourceSpans.map((resourceSpan) => ({ 68 | ...resourceSpan, 69 | instrumentationLibrarySpans: resourceSpan.instrumentationLibrarySpans.map( 70 | (instrumentationLibrarySpans) => ({ 71 | ...instrumentationLibrarySpans, 72 | spans: instrumentationLibrarySpans.spans.map((span) => ({ 73 | ...span, 74 | traceId: hexToBytesArray(span.traceId), 75 | spanId: hexToBytesArray(span.spanId), 76 | parentSpanId: span.parentSpanId ? hexToBytesArray(span.parentSpanId) : span.parentSpanId, 77 | links: span.links.map((link) => ({ 78 | ...link, 79 | traceId: hexToBytesArray(link.traceId), 80 | spanId: hexToBytesArray(link.spanId), 81 | })), 82 | })), 83 | }) 84 | ), 85 | })), 86 | }; 87 | } 88 | -------------------------------------------------------------------------------- /packages/opentelemetry-proto-transformations/src/opentelemetry/proto/common/v1/transform.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@opentelemetry/core'; 2 | import * as api from '@opentelemetry/api'; 3 | 4 | import * as proto from './common'; 5 | 6 | const MAX_INTEGER_VALUE = 2147483647; 7 | const MIN_INTEGER_VALUE = -2147483648; 8 | 9 | export type protoAnyValueType = 10 | | string 11 | | number 12 | | boolean 13 | | Array 14 | | Array 15 | | Array; 16 | 17 | export function toProtoArrayValue(values: protoAnyValueType[]): proto.ArrayValue { 18 | return { 19 | values: values.map(toProtoAnyValue), 20 | }; 21 | } 22 | 23 | export function fromProtoArrayValue(protoArrayValue: proto.ArrayValue): protoAnyValueType { 24 | return protoArrayValue.values.map(fromProtoAnyValue) as protoAnyValueType; 25 | } 26 | 27 | export function toProtoAnyValue(sdkValue: protoAnyValueType): proto.AnyValue { 28 | switch (typeof sdkValue) { 29 | case 'string': 30 | // @ts-ignore 31 | return { 32 | stringValue: sdkValue, 33 | }; 34 | 35 | case 'boolean': 36 | // @ts-ignore 37 | return { 38 | boolValue: sdkValue, 39 | }; 40 | 41 | case 'number': { 42 | if (Number.isInteger(sdkValue) && sdkValue <= MAX_INTEGER_VALUE && sdkValue >= MIN_INTEGER_VALUE) { 43 | // @ts-ignore 44 | return { 45 | intValue: sdkValue, 46 | }; 47 | } else { 48 | // @ts-ignore 49 | return { 50 | doubleValue: sdkValue, 51 | }; 52 | } 53 | } 54 | } 55 | 56 | if (Array.isArray(sdkValue)) { 57 | // @ts-ignore 58 | return { 59 | arrayValue: toProtoArrayValue(sdkValue), 60 | }; 61 | } 62 | 63 | // @ts-ignore 64 | return {}; 65 | } 66 | 67 | export function fromProtoAnyValue(protoAnyValue: proto.AnyValue): protoAnyValueType { 68 | if (protoAnyValue.stringValue) { 69 | return protoAnyValue.stringValue; 70 | } else if (protoAnyValue.boolValue) { 71 | return protoAnyValue.boolValue; 72 | } else if (protoAnyValue.intValue) { 73 | return protoAnyValue.intValue; 74 | } else if (protoAnyValue.doubleValue) { 75 | return protoAnyValue.doubleValue; 76 | } else if (protoAnyValue.arrayValue) { 77 | return fromProtoArrayValue(protoAnyValue.arrayValue); 78 | } 79 | } 80 | 81 | export function toProtoKeyValue( 82 | k: string, 83 | v: 84 | | string 85 | | number 86 | | boolean 87 | | Array 88 | | Array 89 | | Array 90 | ): proto.KeyValue { 91 | return { 92 | key: k, 93 | value: toProtoAnyValue(v), 94 | }; 95 | } 96 | 97 | export function fromProtoKeyValue( 98 | protoKeyValue: proto.KeyValue 99 | ): [ 100 | k: string, 101 | v: 102 | | string 103 | | number 104 | | boolean 105 | | Array 106 | | Array 107 | | Array 108 | ] { 109 | return [protoKeyValue.key, fromProtoAnyValue(protoKeyValue.value)]; 110 | } 111 | 112 | export function toProtoSpanAttributes(sdkSpanAttributes: api.SpanAttributes): proto.KeyValue[] { 113 | return Object.entries(sdkSpanAttributes).map( 114 | ([sdkAttributeKey, sdkAttributeValue]: [any, api.SpanAttributeValue]) => 115 | toProtoKeyValue(sdkAttributeKey, sdkAttributeValue) 116 | ); 117 | } 118 | 119 | export function fromProtoSpanAttributes(protoSpanAttributes: proto.KeyValue[]): api.SpanAttributes { 120 | return Object.fromEntries(protoSpanAttributes.map(fromProtoKeyValue)); 121 | } 122 | 123 | export function toInstrumentationLibrary( 124 | sdkInstrumentationLibrary: core.InstrumentationLibrary 125 | ): proto.InstrumentationLibrary { 126 | return { 127 | name: sdkInstrumentationLibrary.name, 128 | version: sdkInstrumentationLibrary.version, 129 | }; 130 | } 131 | 132 | export function fromInstrumentationLibrary( 133 | protoInstrumentationLibrary: proto.InstrumentationLibrary 134 | ): core.InstrumentationLibrary { 135 | return { 136 | name: protoInstrumentationLibrary.name, 137 | version: protoInstrumentationLibrary.version, 138 | }; 139 | } 140 | -------------------------------------------------------------------------------- /packages/opentelemetry-proto-transformations/src/opentelemetry/proto/resource/v1/resource.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import Long from 'long'; 3 | import _m0 from 'protobufjs/minimal'; 4 | import { KeyValue } from '../../../../opentelemetry/proto/common/v1/common'; 5 | 6 | export const protobufPackage = 'opentelemetry.proto.resource.v1'; 7 | 8 | /** Resource information. */ 9 | export interface Resource { 10 | /** Set of labels that describe the resource. */ 11 | attributes: KeyValue[]; 12 | /** 13 | * dropped_attributes_count is the number of dropped attributes. If the value is 0, then 14 | * no attributes were dropped. 15 | */ 16 | droppedAttributesCount: number; 17 | } 18 | 19 | const baseResource: object = { droppedAttributesCount: 0 }; 20 | 21 | export const Resource = { 22 | encode(message: Resource, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { 23 | for (const v of message.attributes) { 24 | KeyValue.encode(v!, writer.uint32(10).fork()).ldelim(); 25 | } 26 | if (message.droppedAttributesCount !== 0) { 27 | writer.uint32(16).uint32(message.droppedAttributesCount); 28 | } 29 | return writer; 30 | }, 31 | 32 | decode(input: _m0.Reader | Uint8Array, length?: number): Resource { 33 | const reader = input instanceof _m0.Reader ? input : new _m0.Reader(input); 34 | let end = length === undefined ? reader.len : reader.pos + length; 35 | const message = { ...baseResource } as Resource; 36 | message.attributes = []; 37 | while (reader.pos < end) { 38 | const tag = reader.uint32(); 39 | switch (tag >>> 3) { 40 | case 1: 41 | message.attributes.push(KeyValue.decode(reader, reader.uint32())); 42 | break; 43 | case 2: 44 | message.droppedAttributesCount = reader.uint32(); 45 | break; 46 | default: 47 | reader.skipType(tag & 7); 48 | break; 49 | } 50 | } 51 | return message; 52 | }, 53 | 54 | fromJSON(object: any): Resource { 55 | const message = { ...baseResource } as Resource; 56 | message.attributes = []; 57 | if (object.attributes !== undefined && object.attributes !== null) { 58 | for (const e of object.attributes) { 59 | message.attributes.push(KeyValue.fromJSON(e)); 60 | } 61 | } 62 | if (object.droppedAttributesCount !== undefined && object.droppedAttributesCount !== null) { 63 | message.droppedAttributesCount = Number(object.droppedAttributesCount); 64 | } else { 65 | message.droppedAttributesCount = 0; 66 | } 67 | return message; 68 | }, 69 | 70 | toJSON(message: Resource): unknown { 71 | const obj: any = {}; 72 | if (message.attributes) { 73 | obj.attributes = message.attributes.map((e) => (e ? KeyValue.toJSON(e) : undefined)); 74 | } else { 75 | obj.attributes = []; 76 | } 77 | message.droppedAttributesCount !== undefined && (obj.droppedAttributesCount = message.droppedAttributesCount); 78 | return obj; 79 | }, 80 | 81 | fromPartial(object: DeepPartial): Resource { 82 | const message = { ...baseResource } as Resource; 83 | message.attributes = []; 84 | if (object.attributes !== undefined && object.attributes !== null) { 85 | for (const e of object.attributes) { 86 | message.attributes.push(KeyValue.fromPartial(e)); 87 | } 88 | } 89 | if (object.droppedAttributesCount !== undefined && object.droppedAttributesCount !== null) { 90 | message.droppedAttributesCount = object.droppedAttributesCount; 91 | } else { 92 | message.droppedAttributesCount = 0; 93 | } 94 | return message; 95 | }, 96 | }; 97 | 98 | type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; 99 | export type DeepPartial = T extends Builtin 100 | ? T 101 | : T extends Array 102 | ? Array> 103 | : T extends ReadonlyArray 104 | ? ReadonlyArray> 105 | : T extends {} 106 | ? { [K in keyof T]?: DeepPartial } 107 | : Partial; 108 | 109 | if (_m0.util.Long !== Long) { 110 | _m0.util.Long = Long as any; 111 | _m0.configure(); 112 | } 113 | -------------------------------------------------------------------------------- /packages/opentelemetry-proto-transformations/src/opentelemetry/proto/resource/v1/transform.ts: -------------------------------------------------------------------------------- 1 | import * as resources from '@opentelemetry/resources'; 2 | import { fromProtoKeyValue, toProtoKeyValue } from '../../common/v1/transform'; 3 | import * as proto from './resource'; 4 | 5 | export function toProtoResource( 6 | sdkResource: resources.Resource, 7 | additionalAttributes: resources.ResourceAttributes = {} 8 | ): proto.Resource { 9 | const attrs: resources.ResourceAttributes = Object.assign({}, sdkResource.attributes, additionalAttributes); 10 | return { 11 | attributes: Object.entries(attrs).map(([k, v]) => { 12 | return toProtoKeyValue(k, v); 13 | }), 14 | droppedAttributesCount: 0, 15 | }; 16 | } 17 | 18 | export function fromProtoResource(protoResource: proto.Resource): resources.Resource { 19 | return new resources.Resource( 20 | Object.fromEntries( 21 | protoResource.attributes.map((kv) => { 22 | const [resourceAttributeKey, resourceAttributeValue] = fromProtoKeyValue(kv); 23 | return [resourceAttributeKey, resourceAttributeValue as number | string | boolean]; 24 | }) 25 | ) 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/opentelemetry-proto-transformations/src/opentelemetry/proto/trace/v1/transform.ts: -------------------------------------------------------------------------------- 1 | import * as tracing from '@opentelemetry/sdk-trace-base'; 2 | import * as resources from '@opentelemetry/resources'; 3 | import * as core from '@opentelemetry/core'; 4 | import * as api from '@opentelemetry/api'; 5 | 6 | import * as proto from './trace'; 7 | import { fromProtoResource, toProtoResource } from '../../resource/v1/transform'; 8 | import { 9 | fromInstrumentationLibrary, 10 | fromProtoSpanAttributes, 11 | toInstrumentationLibrary, 12 | toProtoKeyValue, 13 | toProtoSpanAttributes, 14 | } from '../../common/v1/transform'; 15 | import { bytesArrayToHex, hexToBytesArray, nanosecondsToHrTime } from '../../../../utils'; 16 | import { hrTimeDuration } from '@opentelemetry/core'; 17 | 18 | function groupSpansByResource(spans: tracing.ReadableSpan[]): Map { 19 | return spans.reduce((byResourceMap, sdkSpan) => { 20 | if (!byResourceMap.has(sdkSpan.resource)) { 21 | byResourceMap.set(sdkSpan.resource, []); 22 | } 23 | byResourceMap.get(sdkSpan.resource).push(sdkSpan); 24 | return byResourceMap; 25 | }, new Map()); 26 | } 27 | 28 | function groupSpansByInstrumentationLibrary( 29 | spans: tracing.ReadableSpan[] 30 | ): Map { 31 | return spans.reduce((byInstrumentationLibraryMap, sdkSpan) => { 32 | if (!byInstrumentationLibraryMap.has(sdkSpan.instrumentationLibrary)) { 33 | byInstrumentationLibraryMap.set(sdkSpan.instrumentationLibrary, []); 34 | } 35 | byInstrumentationLibraryMap.get(sdkSpan.instrumentationLibrary).push(sdkSpan); 36 | return byInstrumentationLibraryMap; 37 | }, new Map()); 38 | } 39 | 40 | export const spanKindToProtoMap = new Map([ 41 | [api.SpanKind.INTERNAL, proto.Span_SpanKind.SPAN_KIND_INTERNAL], 42 | [api.SpanKind.SERVER, proto.Span_SpanKind.SPAN_KIND_SERVER], 43 | [api.SpanKind.CLIENT, proto.Span_SpanKind.SPAN_KIND_CLIENT], 44 | [api.SpanKind.PRODUCER, proto.Span_SpanKind.SPAN_KIND_PRODUCER], 45 | [api.SpanKind.CONSUMER, proto.Span_SpanKind.SPAN_KIND_CONSUMER], 46 | ]); 47 | 48 | export const spanKindFromProtoMap = new Map([ 49 | [proto.Span_SpanKind.SPAN_KIND_INTERNAL, api.SpanKind.INTERNAL], 50 | [proto.Span_SpanKind.SPAN_KIND_SERVER, api.SpanKind.SERVER], 51 | [proto.Span_SpanKind.SPAN_KIND_CLIENT, api.SpanKind.CLIENT], 52 | [proto.Span_SpanKind.SPAN_KIND_PRODUCER, api.SpanKind.PRODUCER], 53 | [proto.Span_SpanKind.SPAN_KIND_CONSUMER, api.SpanKind.CONSUMER], 54 | ]); 55 | 56 | export const spanStatusCodeToProtoMap = new Map([ 57 | [api.SpanStatusCode.UNSET, proto.Status_StatusCode.STATUS_CODE_UNSET], 58 | [api.SpanStatusCode.OK, proto.Status_StatusCode.STATUS_CODE_OK], 59 | [api.SpanStatusCode.ERROR, proto.Status_StatusCode.STATUS_CODE_ERROR], 60 | ]); 61 | 62 | export const spanStatusCodeFromProtoMap = new Map([ 63 | [proto.Status_StatusCode.STATUS_CODE_UNSET, api.SpanStatusCode.UNSET], 64 | [proto.Status_StatusCode.STATUS_CODE_OK, api.SpanStatusCode.OK], 65 | [proto.Status_StatusCode.STATUS_CODE_ERROR, api.SpanStatusCode.ERROR], 66 | ]); 67 | 68 | export function toProtoTraceState(sdkTraceState?: api.TraceState): string | undefined { 69 | if (!sdkTraceState) return undefined; 70 | return sdkTraceState.serialize(); 71 | } 72 | 73 | export function fromProtoTraceState(protoTraceState: string): core.TraceState { 74 | if (!protoTraceState) return undefined; 75 | return new core.TraceState(protoTraceState); 76 | } 77 | 78 | export function toProtoSpanEvent(sdkSpanEvent: tracing.TimedEvent): proto.Span_Event { 79 | return { 80 | timeUnixNano: core.hrTimeToNanoseconds(sdkSpanEvent.time), 81 | name: sdkSpanEvent.name, 82 | attributes: toProtoSpanAttributes(sdkSpanEvent.attributes ?? {}), 83 | droppedAttributesCount: 0, 84 | }; 85 | } 86 | 87 | export function fromProtoSpanEvent(protoSpanEvent: proto.Span_Event): tracing.TimedEvent { 88 | return { 89 | time: nanosecondsToHrTime(protoSpanEvent.timeUnixNano), 90 | name: protoSpanEvent.name, 91 | attributes: fromProtoSpanAttributes(protoSpanEvent.attributes), 92 | }; 93 | } 94 | 95 | export function toProtoSpanLink(sdkLink: api.Link): proto.Span_Link { 96 | return { 97 | traceId: hexToBytesArray(sdkLink.context.traceId), 98 | spanId: hexToBytesArray(sdkLink.context.spanId), 99 | traceState: 'TODO', // https://github.com/open-telemetry/opentelemetry-js-api/issues/36 100 | attributes: toProtoSpanAttributes(sdkLink.attributes ?? {}), 101 | droppedAttributesCount: 0, 102 | }; 103 | } 104 | 105 | export function fromProtoSpanLink(protoSpanLink: proto.Span_Link): api.Link { 106 | return { 107 | context: { 108 | traceId: bytesArrayToHex(protoSpanLink.traceId), 109 | spanId: bytesArrayToHex(protoSpanLink.spanId), 110 | traceFlags: 0, 111 | }, 112 | attributes: fromProtoSpanAttributes(protoSpanLink.attributes), 113 | }; 114 | } 115 | 116 | export function toProtoStatus(sdkSpanStatus: api.SpanStatus): proto.Status { 117 | return { 118 | deprecatedCode: proto.Status_DeprecatedStatusCode.UNRECOGNIZED, 119 | message: sdkSpanStatus.message, 120 | code: spanStatusCodeToProtoMap.get(sdkSpanStatus.code), 121 | }; 122 | } 123 | 124 | export function fromProtoStatus(protoStatus: proto.Status): api.SpanStatus { 125 | return { 126 | code: spanStatusCodeFromProtoMap.get(protoStatus.code), 127 | message: protoStatus.message, 128 | }; 129 | } 130 | 131 | export function toProtoSpan(sdkSpan: tracing.ReadableSpan): proto.Span { 132 | return { 133 | traceId: hexToBytesArray(sdkSpan.spanContext().traceId), 134 | spanId: hexToBytesArray(sdkSpan.spanContext().spanId), 135 | traceState: toProtoTraceState(sdkSpan.spanContext().traceState), 136 | parentSpanId: sdkSpan.parentSpanId ? hexToBytesArray(sdkSpan.parentSpanId) : undefined, 137 | name: sdkSpan.name, 138 | kind: spanKindToProtoMap.get(sdkSpan.kind) ?? proto.Span_SpanKind.SPAN_KIND_UNSPECIFIED, 139 | startTimeUnixNano: core.hrTimeToNanoseconds(sdkSpan.startTime), 140 | endTimeUnixNano: core.hrTimeToNanoseconds(sdkSpan.endTime), 141 | attributes: toProtoSpanAttributes(sdkSpan.attributes), 142 | droppedAttributesCount: 0, 143 | events: sdkSpan.events.map(toProtoSpanEvent), 144 | droppedEventsCount: 0, 145 | links: sdkSpan.links.map(toProtoSpanLink), 146 | droppedLinksCount: 0, 147 | status: toProtoStatus(sdkSpan.status), 148 | }; 149 | } 150 | 151 | export function fromProtoSpan( 152 | protoSpan: proto.Span, 153 | sdkResource: resources.Resource, 154 | sdkInstrumentationLibrary: core.InstrumentationLibrary 155 | ): tracing.ReadableSpan { 156 | const startTime = nanosecondsToHrTime(protoSpan.startTimeUnixNano); 157 | const endTime = nanosecondsToHrTime(protoSpan.endTimeUnixNano); 158 | return { 159 | name: protoSpan.name, 160 | kind: spanKindFromProtoMap.get(protoSpan.kind), 161 | spanContext: () => ({ 162 | traceId: bytesArrayToHex(protoSpan.traceId), 163 | spanId: bytesArrayToHex(protoSpan.spanId), 164 | traceFlags: 0, // we can't actually tell if the trace was sampled since this data is not in the protobuf spec 165 | traceState: fromProtoTraceState(protoSpan.traceState), 166 | }), 167 | parentSpanId: protoSpan.parentSpanId ? bytesArrayToHex(protoSpan.parentSpanId) : undefined, 168 | startTime, 169 | endTime, 170 | status: fromProtoStatus(protoSpan.status), 171 | attributes: fromProtoSpanAttributes(protoSpan.attributes), 172 | links: protoSpan.links.map(fromProtoSpanLink), 173 | events: protoSpan.events.map(fromProtoSpanEvent), 174 | duration: hrTimeDuration(startTime, endTime), 175 | ended: true, 176 | resource: sdkResource, 177 | instrumentationLibrary: sdkInstrumentationLibrary, 178 | droppedAttributesCount: 0, 179 | droppedEventsCount:0, 180 | droppedLinksCount: 0, 181 | }; 182 | } 183 | 184 | export function toProtoInstrumentationLibrarySpans( 185 | sdkInstrumentationLibrary: core.InstrumentationLibrary, 186 | sdkSpans: tracing.ReadableSpan[] 187 | ): proto.InstrumentationLibrarySpans { 188 | return { 189 | instrumentationLibrary: toInstrumentationLibrary(sdkInstrumentationLibrary), 190 | spans: sdkSpans.map((sdkSpan) => toProtoSpan(sdkSpan)), 191 | }; 192 | } 193 | 194 | export function fromProtoInstrumentationLibrarySpans( 195 | protoInstrumentationLibrarySpans: proto.InstrumentationLibrarySpans, 196 | sdkResource: resources.Resource 197 | ): tracing.ReadableSpan[] { 198 | const sdkInstrumentationLibrary = fromInstrumentationLibrary( 199 | protoInstrumentationLibrarySpans.instrumentationLibrary 200 | ); 201 | return protoInstrumentationLibrarySpans.spans.map((protoSpan) => 202 | fromProtoSpan(protoSpan, sdkResource, sdkInstrumentationLibrary) 203 | ); 204 | } 205 | 206 | export function toProtoResourceSpans( 207 | sdkResource: resources.Resource, 208 | sdkResourceSpans: tracing.ReadableSpan[], 209 | additionalAttributes: resources.ResourceAttributes = {} 210 | ): proto.ResourceSpans { 211 | const spansByInstrumentationLibrary = groupSpansByInstrumentationLibrary(sdkResourceSpans); 212 | return { 213 | resource: toProtoResource(sdkResource, additionalAttributes), 214 | instrumentationLibrarySpans: Array.from(spansByInstrumentationLibrary).map( 215 | ([sdkInstrumentationLibrary, sdkInstrumentationLibrarySpans]) => { 216 | return toProtoInstrumentationLibrarySpans(sdkInstrumentationLibrary, sdkInstrumentationLibrarySpans); 217 | } 218 | ), 219 | }; 220 | } 221 | 222 | export function fromProtoResourceSpans(protoResourceSpans: proto.ResourceSpans): tracing.ReadableSpan[] { 223 | const sdkResource = fromProtoResource(protoResourceSpans.resource); 224 | return protoResourceSpans.instrumentationLibrarySpans.flatMap((instrumentationLibrarySpans) => 225 | fromProtoInstrumentationLibrarySpans(instrumentationLibrarySpans, sdkResource) 226 | ); 227 | } 228 | 229 | export function toProtoResourceSpansArray( 230 | sdkSpans: tracing.ReadableSpan[], 231 | additionalAttributes: resources.ResourceAttributes = {} 232 | ): proto.ResourceSpans[] { 233 | const spansByResource = groupSpansByResource(sdkSpans); 234 | return Array.from(spansByResource).map(([sdkResource, sdkResourceSpans]) => 235 | toProtoResourceSpans(sdkResource, sdkResourceSpans, additionalAttributes) 236 | ); 237 | } 238 | 239 | export function fromProtoResourceSpansArray(protoResourceSpansArray: proto.ResourceSpans[]): tracing.ReadableSpan[] { 240 | return protoResourceSpansArray.flatMap(fromProtoResourceSpans); 241 | } 242 | -------------------------------------------------------------------------------- /packages/opentelemetry-proto-transformations/src/utils.ts: -------------------------------------------------------------------------------- 1 | import * as api from '@opentelemetry/api'; 2 | 3 | export function hexToBytesArray(hexStr: string): Uint8Array { 4 | const hexStrLen = hexStr.length; 5 | let bytesArray: number[] = []; 6 | for (let i = 0; i < hexStrLen; i += 2) { 7 | const hexPair = hexStr.substring(i, i + 2); 8 | const hexVal = parseInt(hexPair, 16); 9 | bytesArray.push(hexVal); 10 | } 11 | return Uint8Array.from(bytesArray); 12 | } 13 | 14 | export function bytesArrayToHex(bytes: Uint8Array): string { 15 | return Array.from(bytes, (byte) => { 16 | return ('0' + (byte & 0xff).toString(16)).slice(-2); 17 | }).join(''); 18 | } 19 | 20 | const NANOSECOND_DIGITS = 9; 21 | const SECOND_TO_NANOSECONDS = Math.pow(10, NANOSECOND_DIGITS); 22 | 23 | export function nanosecondsToHrTime(nanosecondsTime: number): api.HrTime { 24 | return [Math.floor(nanosecondsTime / SECOND_TO_NANOSECONDS), nanosecondsTime % SECOND_TO_NANOSECONDS]; 25 | } 26 | -------------------------------------------------------------------------------- /packages/opentelemetry-proto-transformations/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "./dist", 6 | "lib": ["es2019"], 7 | "target": "es5", 8 | "esModuleInterop": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/telemetry-repository/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "malabi-telemetry-repository", 3 | "version": "0.0.7-alpha.1", 4 | "author": "Aspecto Authors", 5 | "homepage": "https://github.com/aspecto-io/malabi#readme", 6 | "license": "Apache-2.0", 7 | "main": "dist/src/index.js", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/aspecto-io/malabi.git" 11 | }, 12 | "scripts": { 13 | "build": "tsc", 14 | "watch": "tsc -w", 15 | "test": "mocha" 16 | }, 17 | "files": [ 18 | "dist/src/**/*.js", 19 | "dist/src/**/*.d.ts", 20 | "LICENSE", 21 | "README.md" 22 | ], 23 | "bugs": { 24 | "url": "https://github.com/aspecto-io/malabi/issues" 25 | }, 26 | "dependencies": { 27 | "@opentelemetry/api": "^1.8.0", 28 | "@opentelemetry/semantic-conventions": "^1.24.1", 29 | "@opentelemetry/sdk-trace-base": "^1.24.1" 30 | }, 31 | "devDependencies": { 32 | "@types/mocha": "^8.2.2", 33 | "@types/node": "^15.6.0", 34 | "expect": "^26.6.2", 35 | "mocha": "^8.3.2", 36 | "ts-node": "^10.9.2", 37 | "typescript": "^4.2.4" 38 | }, 39 | "mocha": { 40 | "extension": [ 41 | "ts" 42 | ], 43 | "spec": "test/**/*.spec.ts", 44 | "require": "ts-node/register" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/telemetry-repository/src/MalabiSpan.ts: -------------------------------------------------------------------------------- 1 | import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; 2 | import { SpanStatusCode } from '@opentelemetry/api'; 3 | import { SemanticAttributes } from '@opentelemetry/semantic-conventions'; 4 | 5 | export class MalabiSpan { 6 | private span: ReadableSpan; 7 | 8 | constructor(span: ReadableSpan) { 9 | this.span = span; 10 | } 11 | 12 | private strAttr(attr: string): string { 13 | return this.span.attributes[attr] as string; 14 | } 15 | 16 | // === Misc === 17 | 18 | get raw() { 19 | return this.span; 20 | } 21 | 22 | get hasError() { 23 | return this.span.status.code === SpanStatusCode.ERROR; 24 | } 25 | 26 | get errorMessage() { 27 | return this.span.status.message; 28 | } 29 | 30 | get name() { 31 | return this.span.name; 32 | } 33 | 34 | get kind() { 35 | return this.span.kind; 36 | } 37 | 38 | attr(attr: string) { 39 | return this.span.attributes[attr]; 40 | } 41 | 42 | attribute(attr: string) { 43 | return this.span.attributes[attr]; 44 | } 45 | 46 | // === Network === 47 | 48 | get netPeerName() { 49 | return this.strAttr(SemanticAttributes.NET_PEER_NAME); 50 | } 51 | 52 | get netTransport() { 53 | return this.strAttr(SemanticAttributes.NET_TRANSPORT); 54 | } 55 | 56 | get netPeerPort() { 57 | return this.attr(SemanticAttributes.NET_PEER_PORT) as number; 58 | } 59 | 60 | // === HTTP === 61 | 62 | get httpMethod() { 63 | return this.strAttr(SemanticAttributes.HTTP_METHOD); 64 | } 65 | 66 | get httpFullUrl() { 67 | return this.strAttr(SemanticAttributes.HTTP_URL); 68 | } 69 | 70 | get httpHost() { 71 | return this.strAttr(SemanticAttributes.HTTP_HOST); 72 | } 73 | 74 | get httpRoute() { 75 | return this.strAttr(SemanticAttributes.HTTP_ROUTE); 76 | } 77 | 78 | get httpUserAgent() { 79 | return this.strAttr(SemanticAttributes.HTTP_USER_AGENT); 80 | } 81 | 82 | get statusCode() { 83 | const strStatusCode = this.strAttr(SemanticAttributes.HTTP_STATUS_CODE); 84 | return strStatusCode ? parseInt(strStatusCode) : undefined; 85 | } 86 | 87 | get requestBody() { 88 | return this.strAttr('http.request.body'); 89 | } 90 | 91 | get responseBody() { 92 | return this.strAttr('http.response.body'); 93 | } 94 | 95 | private parseHeaders(attKey: string) { 96 | const headers = this.strAttr(attKey); 97 | if (!headers) return null; 98 | try { 99 | const parsed = JSON.parse(headers); 100 | const lowerCaseHeaders = Object.fromEntries( 101 | Object.entries(parsed).map(([k, v]) => [k.toLowerCase(), v as string]) 102 | ); 103 | return lowerCaseHeaders; 104 | } catch (err) { 105 | throw new Error('Headers structure is invalid.'); 106 | } 107 | } 108 | 109 | get requestHeaders(): Record { 110 | return this.parseHeaders('http.request.headers'); 111 | } 112 | 113 | get responseHeaders(): Record { 114 | return this.parseHeaders('http.response.headers'); 115 | } 116 | 117 | requestHeader(header: string) { 118 | const headers = this.requestHeaders; 119 | return headers ? headers[header.toLowerCase()] : null; 120 | } 121 | 122 | responseHeader(header: string) { 123 | const headers = this.responseHeaders; 124 | return headers ? headers[header.toLowerCase()] : null; 125 | } 126 | 127 | get queryParams() { 128 | const url = this.httpFullUrl as string; 129 | if (!url) return null; 130 | 131 | return Object.fromEntries(new URL(url).searchParams as any); 132 | } 133 | 134 | queryParam(param: string): string { 135 | return this.queryParams[param]; 136 | } 137 | 138 | // === DataBase === 139 | 140 | get dbSystem() { 141 | return this.strAttr(SemanticAttributes.DB_SYSTEM); 142 | } 143 | 144 | get dbUser() { 145 | return this.strAttr(SemanticAttributes.DB_USER); 146 | } 147 | 148 | get dbName() { 149 | return this.strAttr(SemanticAttributes.DB_NAME); 150 | } 151 | 152 | get dbOperation() { 153 | return this.strAttr(SemanticAttributes.DB_OPERATION); 154 | } 155 | 156 | get dbStatement() { 157 | return this.strAttr(SemanticAttributes.DB_STATEMENT); 158 | } 159 | 160 | get mongoCollection() { 161 | return this.strAttr(SemanticAttributes.DB_MONGODB_COLLECTION); 162 | } 163 | 164 | get dbResponse() { 165 | return this.strAttr('db.response'); 166 | } 167 | // === Messaging === 168 | 169 | get messagingSystem() { 170 | return this.strAttr(SemanticAttributes.MESSAGING_SYSTEM); 171 | } 172 | 173 | get messagingDestinationKind() { 174 | return this.strAttr(SemanticAttributes.MESSAGING_DESTINATION_KIND); 175 | } 176 | 177 | get queueOrTopicName() { 178 | return this.strAttr(SemanticAttributes.MESSAGING_DESTINATION); 179 | } 180 | 181 | get queueOrTopicUrl() { 182 | return this.strAttr(SemanticAttributes.MESSAGING_URL); 183 | } 184 | 185 | get messagingOperation() { 186 | return this.strAttr(SemanticAttributes.MESSAGING_OPERATION); 187 | } 188 | 189 | get messagingPayload() { 190 | return this.strAttr('messaging.payload'); 191 | } 192 | 193 | // === RPC === 194 | 195 | get rpcSystem() { 196 | return this.strAttr(SemanticAttributes.RPC_SYSTEM); 197 | } 198 | 199 | get rpcService() { 200 | return this.strAttr(SemanticAttributes.RPC_SERVICE); 201 | } 202 | 203 | get rpcMethod() { 204 | return this.strAttr(SemanticAttributes.RPC_METHOD); 205 | } 206 | 207 | get awsRequestParams() { 208 | return this.strAttr('aws.request.params'); 209 | } 210 | } 211 | -------------------------------------------------------------------------------- /packages/telemetry-repository/src/SpansRepository.ts: -------------------------------------------------------------------------------- 1 | import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; 2 | import { MalabiSpan } from './MalabiSpan'; 3 | import { MessagingOperationValues, SemanticAttributes } from '@opentelemetry/semantic-conventions'; 4 | import { SpanKind } from '@opentelemetry/api'; 5 | 6 | /** 7 | * A Class that allows access of spans created in the test run. for example: HTTP GET spans. Mongo db spans, etc. 8 | * Read more about OpenTelemetry Spans [here]{@link https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md#span} 9 | */ 10 | class SpansRepository { 11 | readonly spans: ReadableSpan[]; 12 | 13 | /** 14 | * Initializes the internal spans array. 15 | * @param spans An array of ReadableSpans 16 | */ 17 | constructor(spans: ReadableSpan[]) { 18 | this.spans = spans; 19 | } 20 | 21 | /** 22 | * Filters the internal spans array according to the predicate. 23 | * Returns a new SpansRepository object to allow chaining. 24 | * @param predicate A predicate to filter the spans. 25 | */ 26 | private filter(predicate: (span: ReadableSpan) => boolean): SpansRepository { 27 | return new SpansRepository(this.spans.filter(predicate)); 28 | } 29 | 30 | /** 31 | * Returns how many spans are currently in the array. 32 | */ 33 | get length() { 34 | return this.spans.length; 35 | } 36 | 37 | /** 38 | * Returns the first span as MalabiSpan. 39 | */ 40 | get first() { 41 | if (this.spans.length < 1) throw new Error(`Tried to get the "first" span, but there are no spans.`); 42 | return new MalabiSpan(this.spans[0]); 43 | } 44 | 45 | /** 46 | * Returns the second span as MalabiSpan. 47 | */ 48 | get second() { 49 | if (this.spans.length < 2) 50 | throw new Error(`Tried to get the "second" span, but there are only ${this.spans.length} spans.`); 51 | return new MalabiSpan(this.spans[1]); 52 | } 53 | 54 | /** 55 | * Returns all spans as MalabiSpan. 56 | */ 57 | get all(): MalabiSpan[] { 58 | return this.spans.map((s) => new MalabiSpan(s)); 59 | } 60 | 61 | /** 62 | * Returns span at the given index as MalabiSpan. 63 | * @param index The index to retrieve. 64 | */ 65 | at(index: number) { 66 | if (this.spans.length < index + 1) 67 | throw new Error(`Tried to get the span in ${index} index, but there are only ${this.spans.length} spans.`); 68 | return new MalabiSpan(this.spans[index]); 69 | } 70 | 71 | /** 72 | * Returns a new instance of SpansRepository with only HTTP spans. 73 | */ 74 | http() { 75 | return this.filter((span) => Boolean(span.attributes[SemanticAttributes.HTTP_METHOD])); 76 | } 77 | 78 | /** 79 | * Returns a new instance of SpansRepository with only HTTP spans, filtered by method. 80 | * @param The method to filter by. GET,POST, etc. 81 | */ 82 | httpMethod(method: string) { 83 | return this.filter( 84 | (span) => 85 | (span.attributes[SemanticAttributes.HTTP_METHOD] as string)?.toLowerCase() === method.toLowerCase() 86 | ); 87 | } 88 | 89 | 90 | /** 91 | * Returns a new instance of SpansRepository with only HTTP GET spans. 92 | */ 93 | httpGet() { 94 | return this.filter( 95 | (span) => (span.attributes[SemanticAttributes.HTTP_METHOD] as string)?.toLowerCase() === 'get' 96 | ); 97 | } 98 | 99 | /** 100 | * Returns a new instance of SpansRepository with only HTTP POST spans. 101 | */ 102 | httpPost() { 103 | return this.filter( 104 | (span) => (span.attributes[SemanticAttributes.HTTP_METHOD] as string)?.toLowerCase() === 'post' 105 | ); 106 | } 107 | 108 | /** 109 | * Returns a new instance of SpansRepository with only HTTP spans that have a specific route 110 | * @param r The route to filter by. 111 | */ 112 | route(r: string | RegExp) { 113 | return this.filter((span) => { 114 | const route = span[SemanticAttributes.HTTP_ROUTE] as string; 115 | return typeof r === 'string' ? route?.toLowerCase() === r.toLowerCase() : r.test(route); 116 | }); 117 | } 118 | 119 | /** 120 | * Returns a new instance of SpansRepository with only HTTP spans that have a specific path 121 | * @param p The path to filter by. 122 | */ 123 | path(p: string | RegExp) { 124 | return this.filter((span) => { 125 | const path = span.attributes['http.path'] as string; 126 | return typeof p === 'string' ? path?.toLowerCase() === p.toLowerCase() : p.test(path); 127 | }); 128 | } 129 | 130 | /** 131 | * Returns a new instance of SpansRepository with only messaging spans with the following kind: producer. 132 | */ 133 | messagingSend() { 134 | return this.filter((span) => span.kind === SpanKind.PRODUCER); 135 | } 136 | 137 | /** 138 | * Returns a new instance of SpansRepository with only messaging spans with the following MESSAGING_OPERATION: receive. 139 | */ 140 | messagingReceive() { 141 | return this.filter( 142 | (span) => span.attributes[SemanticAttributes.MESSAGING_OPERATION] === MessagingOperationValues.RECEIVE 143 | ); 144 | } 145 | 146 | /** 147 | * Returns a new instance of SpansRepository with only messaging spans with the following MESSAGING_OPERATION: process. 148 | */ 149 | messagingProcess() { 150 | return this.filter( 151 | (span) => span.attributes[SemanticAttributes.MESSAGING_OPERATION] === MessagingOperationValues.PROCESS 152 | ); 153 | } 154 | 155 | 156 | /** 157 | * Returns a new instance of SpansRepository with SQS spans only. 158 | */ 159 | awsSqs() { 160 | return this.filter((span) => span.attributes[SemanticAttributes.RPC_SERVICE] === 'sqs'); 161 | } 162 | 163 | /** 164 | * Returns a new instance of SpansRepository with the entry span only (meaning without parent span id). 165 | */ 166 | entry() { 167 | return this.filter((span) => !span.parentSpanId); 168 | } 169 | 170 | /** 171 | * Returns a new instance of SpansRepository mongo db spans only. 172 | */ 173 | mongo() { 174 | return this.filter((span) => span.attributes[SemanticAttributes.DB_SYSTEM] === 'mongodb'); 175 | } 176 | 177 | /** 178 | * Returns a new instance of SpansRepository with incoming spans only. 179 | */ 180 | incoming() { 181 | return this.filter((span) => span.kind === SpanKind.SERVER); 182 | } 183 | 184 | /** 185 | * Returns a new instance of SpansRepository with outgoing spans only. 186 | */ 187 | outgoing() { 188 | return this.filter((span) => span.kind === SpanKind.CLIENT); 189 | } 190 | 191 | /** 192 | * Returns a new instance of SpansRepository with express spans only. 193 | */ 194 | express() { 195 | return this.filter((span) => span.instrumentationLibrary.name.includes('express')); 196 | } 197 | 198 | /** 199 | * Returns a new instance of SpansRepository with TypeORM spans only. 200 | */ 201 | typeorm() { 202 | return this.filter((span) => span.instrumentationLibrary.name.includes('typeorm')); 203 | } 204 | 205 | /** 206 | * Returns a new instance of SpansRepository with sequelize spans only. 207 | */ 208 | sequelize() { 209 | return this.filter((span) => span.instrumentationLibrary.name.includes('sequelize')); 210 | } 211 | 212 | /** 213 | * Returns a new instance of SpansRepository with Neo4j spans only. 214 | */ 215 | neo4j() { 216 | return this.filter((span) => span.instrumentationLibrary.name.includes('neo4j')); 217 | } 218 | 219 | /** 220 | * Returns a new instance of SpansRepository with database spans only. 221 | */ 222 | database() { 223 | return this.filter((span) => Boolean(span.attributes[SemanticAttributes.DB_SYSTEM])); 224 | } 225 | 226 | /** 227 | * Returns a new instance of SpansRepository with database spans that match a given operation. 228 | * @param op The operation to filter by. For example: "save". 229 | */ 230 | dbOperation(op: string) { 231 | return this.filter( 232 | (span) => (span.attributes[SemanticAttributes.DB_OPERATION] as string)?.toLowerCase() === op.toLowerCase() 233 | ); 234 | } 235 | 236 | /** 237 | * Returns a new instance of SpansRepository with messaging spans only. 238 | */ 239 | messaging() { 240 | return this.filter((span) => Boolean(span.attributes[SemanticAttributes.MESSAGING_SYSTEM])); 241 | } 242 | 243 | /** 244 | * Returns a new instance of SpansRepository with rpc spans only. 245 | */ 246 | rpc() { 247 | return this.filter((span) => Boolean(span.attributes[SemanticAttributes.RPC_SYSTEM])); 248 | } 249 | 250 | /** 251 | * Returns a new instance of SpansRepository with Redis spans only. 252 | */ 253 | redis (){ 254 | return this.filter((span) => span.attributes[SemanticAttributes.DB_SYSTEM] === 'redis'); 255 | } 256 | 257 | /** 258 | * Returns a new instance of SpansRepository with AWS spans only. 259 | */ 260 | aws() { 261 | return this.filter((span) => span.attributes[SemanticAttributes.RPC_SYSTEM] === 'aws-api'); 262 | } 263 | } 264 | 265 | export default SpansRepository; 266 | -------------------------------------------------------------------------------- /packages/telemetry-repository/src/TelemetryRepository.ts: -------------------------------------------------------------------------------- 1 | import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; 2 | import SpansRepository from './SpansRepository'; 3 | 4 | /** 5 | * A Class that allows access of telemetry from the test run. for example: HTTP GET spans. Mongo db spans, etc. 6 | * 7 | * Read more about OpenTelemetry [here]{@link https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/trace/api.md} 8 | */ 9 | export class TelemetryRepository { 10 | private readonly spansRepository: SpansRepository; 11 | 12 | /** 13 | * Fetches the spans from the exposed malabi spans endpoint 14 | * @param spans An array of ReadableSpans 15 | */ 16 | constructor(spans: ReadableSpan[]) { 17 | this.spansRepository = new SpansRepository(spans); 18 | } 19 | 20 | /** 21 | * Get the SpansRepository object that allows you to do filtering on the spans. chaining is filters supported. 22 | */ 23 | get spans() { 24 | return this.spansRepository; 25 | } 26 | } 27 | 28 | export const initRepository = (spans: ReadableSpan[]) => new TelemetryRepository(spans); 29 | -------------------------------------------------------------------------------- /packages/telemetry-repository/src/index.ts: -------------------------------------------------------------------------------- 1 | export { TelemetryRepository, initRepository } from './TelemetryRepository'; 2 | export { MalabiSpan } from './MalabiSpan'; 3 | -------------------------------------------------------------------------------- /packages/telemetry-repository/test/MalabiSpan.spec.ts: -------------------------------------------------------------------------------- 1 | import 'mocha'; 2 | import { MalabiSpan } from '../src/MalabiSpan'; 3 | import { ReadableSpan } from '@opentelemetry/sdk-trace-base'; 4 | import { SpanKind, SpanStatusCode } from '@opentelemetry/api'; 5 | import expect from 'expect'; 6 | 7 | describe('MalabiSpan', () => { 8 | const span: ReadableSpan = { 9 | attributes: { 10 | 'messaging.system': 'aws.sqs', 11 | 'messaging.destination_kind': 'queue', 12 | 'messaging.destination': 'my-queue', 13 | 'messaging.url': 'http://localstack:4566/000000000000/my-queue', 14 | 'messaging.payload': '{"title":"Playground","pageId":43579833}', 15 | 'messaging.operation': 'process', 16 | 'http.url': 'http://localhost:8000/auth/profile?token=1234', 17 | 'http.host': 'localhost:8000', 18 | 'http.method': 'GET', 19 | 'http.route': '/auth/profile', 20 | 'http.user_agent': 'Chrome', 21 | 'http.status_code': 401, 22 | 'span.kind': 'internal', 23 | 'http.request.headers': '{"Host":"localhost:8000","coNNecTion":"Keep-Alive"}', 24 | 'http.request.body': 'hello', 25 | 'http.response.headers': '{"x-powered-by":"Express","Access-Control-Allow-Origin":"*"}', 26 | 'http.response.body': '{"_id":"60643994b0e6810024914e60","username":"homer","firstName":"Homer","lastName":"Simpson","email":"doh@gmail.com"}', 27 | 'db.name': 'my-db', 28 | 'db.user': 'shlomo', 29 | 'db.system': 'mongodb', 30 | 'db.operation': 'createIndexes', 31 | 'db.mongodb.collection': 'my-collection', 32 | 'db.statement': 'some-statement', 33 | 'db.response': 'some-response', 34 | 'rpc.system': 'aws-api', 35 | 'rpc.service': 's3', 36 | 'rpc.method': 'putObject', 37 | 'aws.request.params': 'some-aws-req-params', 38 | 'some-attr': 'david', 39 | }, 40 | duration: [123, 123], 41 | endTime: [123, 123], 42 | ended: true, 43 | events: [], 44 | instrumentationLibrary: { 45 | name: 'test', 46 | }, 47 | kind: SpanKind.CLIENT, 48 | links: [], 49 | name: 'name', 50 | resource: null, 51 | startTime: [123, 123], 52 | status: { 53 | code: SpanStatusCode.ERROR, 54 | message: 'Some Error Message!', 55 | }, 56 | spanContext: null, 57 | droppedAttributesCount: 0, 58 | droppedEventsCount: 0, 59 | droppedLinksCount: 0 60 | }; 61 | 62 | const malabiSpan = new MalabiSpan(span); 63 | 64 | it('raw', () => { 65 | expect(malabiSpan.raw).toBe(span); 66 | }); 67 | 68 | it('hasError', () => { 69 | expect(malabiSpan.hasError).toBe(true); 70 | }); 71 | 72 | it('errorMessage', () => { 73 | expect(malabiSpan.errorMessage).toBe('Some Error Message!'); 74 | }); 75 | 76 | it('attr', () => { 77 | expect(malabiSpan.attr('some-attr')).toBe('david'); 78 | }); 79 | 80 | it('attribute', () => { 81 | expect(malabiSpan.attribute('some-attr')).toBe('david'); 82 | }); 83 | 84 | it('httpMethod', () => { 85 | expect(malabiSpan.httpMethod).toBe('GET'); 86 | }); 87 | 88 | it('httpFullUrl', () => { 89 | expect(malabiSpan.httpFullUrl).toBe('http://localhost:8000/auth/profile?token=1234'); 90 | }); 91 | 92 | it('httpHost', () => { 93 | expect(malabiSpan.httpHost).toBe('localhost:8000'); 94 | }); 95 | 96 | it('httpRoute', () => { 97 | expect(malabiSpan.httpRoute).toBe('/auth/profile'); 98 | }); 99 | 100 | it('httpUserAgent', () => { 101 | expect(malabiSpan.httpUserAgent).toBe('Chrome'); 102 | }); 103 | 104 | it('statusCode', () => { 105 | expect(malabiSpan.statusCode).toBe(401); 106 | }); 107 | 108 | it('requestBody', () => { 109 | expect(malabiSpan.requestBody).toBe('hello'); 110 | }); 111 | 112 | it('responseBody', () => { 113 | expect(malabiSpan.responseBody).toBe(span.attributes['http.response.body']); 114 | }); 115 | 116 | it('requestHeaders - parses and changes keys to lower case', () => { 117 | expect(malabiSpan.requestHeaders).toEqual({ connection: 'Keep-Alive', host: 'localhost:8000' }); 118 | }); 119 | 120 | it('responseHeaders - parses and changes keys to lower case', () => { 121 | expect(malabiSpan.responseHeaders).toEqual({ 'access-control-allow-origin': '*', 'x-powered-by': 'Express' }); 122 | }); 123 | 124 | it('requestHeader - case agnostic', () => { 125 | expect(malabiSpan.requestHeader('Connection')).toBe('Keep-Alive'); 126 | expect(malabiSpan.requestHeader('connection')).toBe('Keep-Alive'); 127 | expect(malabiSpan.requestHeader('coNneCtioN')).toBe('Keep-Alive'); 128 | expect(malabiSpan.requestHeader('not-there')).toBe(undefined); 129 | }); 130 | 131 | it('responseHeader - case agnostic', () => { 132 | expect(malabiSpan.responseHeader('x-powered-by')).toBe('Express'); 133 | expect(malabiSpan.responseHeader('X-Powered-By')).toBe('Express'); 134 | expect(malabiSpan.responseHeader('X-POWERED-BY')).toBe('Express'); 135 | expect(malabiSpan.responseHeader('not-there')).toBe(undefined); 136 | }); 137 | 138 | it('queryParams - parses', () => { 139 | expect(malabiSpan.queryParams).toEqual({ token: '1234' }); 140 | }); 141 | 142 | it('queryParam', () => { 143 | expect(malabiSpan.queryParam('token')).toBe('1234'); 144 | expect(malabiSpan.queryParam('other')).toBe(undefined); 145 | }); 146 | 147 | it('dbSystem', () => { 148 | expect(malabiSpan.dbSystem).toBe('mongodb'); 149 | }); 150 | 151 | it('dbUser', () => { 152 | expect(malabiSpan.dbUser).toBe('shlomo'); 153 | }); 154 | 155 | it('dbName', () => { 156 | expect(malabiSpan.dbName).toBe('my-db'); 157 | }); 158 | 159 | it('dbOperation', () => { 160 | expect(malabiSpan.dbOperation).toBe('createIndexes'); 161 | }); 162 | 163 | it('dbStatement', () => { 164 | expect(malabiSpan.dbStatement).toBe('some-statement'); 165 | }); 166 | 167 | it('mongoCollection', () => { 168 | expect(malabiSpan.mongoCollection).toBe('my-collection'); 169 | }); 170 | 171 | it('dbResponse', () => { 172 | expect(malabiSpan.dbResponse).toBe('some-response'); 173 | }); 174 | 175 | it('messagingSystem', () => { 176 | expect(malabiSpan.messagingSystem).toBe('aws.sqs'); 177 | }); 178 | 179 | it('messagingDestinationKind', () => { 180 | expect(malabiSpan.messagingDestinationKind).toBe('queue'); 181 | }); 182 | 183 | it('queueOrTopicName', () => { 184 | expect(malabiSpan.queueOrTopicName).toBe('my-queue'); 185 | }); 186 | 187 | it('queueOrTopicUrl', () => { 188 | expect(malabiSpan.queueOrTopicUrl).toBe('http://localstack:4566/000000000000/my-queue'); 189 | }); 190 | 191 | it('messagingOperation', () => { 192 | expect(malabiSpan.messagingOperation).toBe('process'); 193 | }); 194 | 195 | it('messagingPayload', () => { 196 | expect(malabiSpan.messagingPayload).toBe('{"title":"Playground","pageId":43579833}'); 197 | }); 198 | 199 | it('rpcSystem', () => { 200 | expect(malabiSpan.rpcSystem).toBe('aws-api'); 201 | }); 202 | 203 | it('rpcService', () => { 204 | expect(malabiSpan.rpcService).toBe('s3'); 205 | }); 206 | 207 | it('rpcMethod', () => { 208 | expect(malabiSpan.rpcMethod).toBe('putObject'); 209 | }); 210 | 211 | it('awsRequestParams', () => { 212 | expect(malabiSpan.awsRequestParams).toBe('some-aws-req-params'); 213 | }); 214 | }); 215 | -------------------------------------------------------------------------------- /packages/telemetry-repository/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.base.json", 3 | "compilerOptions": { 4 | "rootDir": ".", 5 | "outDir": "dist" 6 | }, 7 | "include": ["src/**/*.ts", "test/**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["ES2019", "DOM"], 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "strict": false, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "sourceMap": true, 11 | "composite": true, 12 | "inlineSources": true, 13 | "types": ["node"], 14 | "allowSyntheticDefaultImports": true, 15 | "esModuleInterop": true, 16 | "skipLibCheck": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /scripts/version-update.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const os = require('os'); 3 | const path = require('path'); 4 | 5 | const appRoot = process.cwd(); 6 | const packageJsonUrl = path.resolve(`${appRoot}/package.json`); 7 | const pjson = require(packageJsonUrl); 8 | 9 | const content = `// this is autogenerated file, see scripts/version-update.js 10 | export const VERSION = '${pjson.version}'; 11 | `; 12 | 13 | const fileUrl = path.join(appRoot, 'src', 'version.ts'); 14 | fs.writeFileSync(fileUrl, content.replace(/\n/g, os.EOL)); 15 | --------------------------------------------------------------------------------