├── .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 |
3 |
4 |
5 | OpenTelemetry based Javascript test framework
6 |
7 |
8 |
9 |
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 |
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 Class TelemetryRepository Constructors constructor Defined in telemetry-repository/dist/src/TelemetryRepository.d.ts:14 Parameters Accessors spans Defined in telemetry-repository/dist/src/TelemetryRepository.d.ts:18 Legend Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules.html:
--------------------------------------------------------------------------------
1 | malabi Main Functions Functions instrument instrument( InstrumentationConfig: InstrumentationConfig ) : void Parameters InstrumentationConfig: InstrumentationConfig Returns void Other Functions serve Malabi From Http App serve Malabi From Http App( port: number , instrumentationConfig: InstrumentationConfig ) : any Parameters port: number instrumentationConfig: InstrumentationConfig Returns any Legend Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/docs/modules/_internal_.html:
--------------------------------------------------------------------------------
1 | <internal> | malabi Legend Settings Theme OS Light Dark
--------------------------------------------------------------------------------
/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 |
3 |
4 |
5 |
6 | OpenTelemetry based Javascript test framework
7 |
8 |
9 |
10 |
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 |
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 | [](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 |
--------------------------------------------------------------------------------
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 |