From ec0a32483c9998e9fff9881204d63e4dea53b3f1 Mon Sep 17 00:00:00 2001 From: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com> Date: Mon, 20 Jul 2026 12:09:30 +0200 Subject: [PATCH] [FEATURE] add trace heatmap chart plugin Signed-off-by: Mahmoud Shahrokni Signed-off-by: Mahmoud Shahrokni Signed-off-by: Mahmoud Shahrokni Signed-off-by: Mahmoud Shahrokni Signed-off-by: Mahmoud Shahrokni Signed-off-by: Mahmoud Shahrokni Signed-off-by: Mahmoud Shahrokni Signed-off-by: Mahmoud Shahrokni Signed-off-by: Mahmoud Shahrokni Signed-off-by: Mahmoud Shahrokni Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com> Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com> Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com> Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com> Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com> Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com> Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com> Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com> Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com> Signed-off-by: Seyed Mahmoud Shahrokni <39291137+shahrokni@users.noreply.github.com> --- package-lock.json | 27 + package.json | 1 + scripts/npm/npm.go | 2 +- traceheatmapchart/.cjs.swcrc | 20 + traceheatmapchart/.eslintrc.js | 78 ++ traceheatmapchart/.gitignore | 21 + traceheatmapchart/.swcrc | 21 + traceheatmapchart/LICENSE | 201 +++++ traceheatmapchart/README.md | 43 + traceheatmapchart/cue.mod/module.cue | 17 + traceheatmapchart/go.mod | 30 + traceheatmapchart/go.sum | 66 ++ traceheatmapchart/jest.config.ts | 23 + traceheatmapchart/package.json | 63 ++ traceheatmapchart/rsbuild.config.ts | 42 + .../schemas/traceheatmapchart.cue | 24 + traceheatmapchart/src/TraceheatmapChart.ts | 25 + .../src/TraceheatmapChartPanel.tsx | 146 ++++ .../src/TraceheatmapSettingsEditor.tsx | 63 ++ traceheatmapchart/src/bootstrap.tsx | 18 + traceheatmapchart/src/env.d.ts | 14 + traceheatmapchart/src/getPluginModule.ts | 30 + traceheatmapchart/src/index-federation.ts | 14 + traceheatmapchart/src/index.ts | 14 + traceheatmapchart/src/setup-tests.ts | 17 + .../src/trace-search-result-mock.ts | 815 ++++++++++++++++++ .../src/traceheatmap-chart-data-util.test.ts | 27 + .../src/traceheatmap-chart-data-util.ts | 140 +++ .../src/traceheatmap-chart-model.ts | 26 + .../traceheatmap-duration-bucket-util.test.ts | 102 +++ .../src/traceheatmap-duration-bucket-util.ts | 47 + .../src/traceheatmap-time-bucket-util.test.ts | 186 ++++ .../src/traceheatmap-time-bucket-util.ts | 220 +++++ .../src/traceheatmap-tooltip-util.test.ts | 192 +++++ .../src/traceheatmap-tooltip-util.ts | 119 +++ traceheatmapchart/tsconfig.build.json | 9 + traceheatmapchart/tsconfig.json | 23 + 37 files changed, 2925 insertions(+), 1 deletion(-) create mode 100644 traceheatmapchart/.cjs.swcrc create mode 100644 traceheatmapchart/.eslintrc.js create mode 100644 traceheatmapchart/.gitignore create mode 100644 traceheatmapchart/.swcrc create mode 100644 traceheatmapchart/LICENSE create mode 100644 traceheatmapchart/README.md create mode 100644 traceheatmapchart/cue.mod/module.cue create mode 100644 traceheatmapchart/go.mod create mode 100644 traceheatmapchart/go.sum create mode 100644 traceheatmapchart/jest.config.ts create mode 100644 traceheatmapchart/package.json create mode 100644 traceheatmapchart/rsbuild.config.ts create mode 100644 traceheatmapchart/schemas/traceheatmapchart.cue create mode 100644 traceheatmapchart/src/TraceheatmapChart.ts create mode 100644 traceheatmapchart/src/TraceheatmapChartPanel.tsx create mode 100644 traceheatmapchart/src/TraceheatmapSettingsEditor.tsx create mode 100644 traceheatmapchart/src/bootstrap.tsx create mode 100644 traceheatmapchart/src/env.d.ts create mode 100644 traceheatmapchart/src/getPluginModule.ts create mode 100644 traceheatmapchart/src/index-federation.ts create mode 100644 traceheatmapchart/src/index.ts create mode 100644 traceheatmapchart/src/setup-tests.ts create mode 100644 traceheatmapchart/src/trace-search-result-mock.ts create mode 100644 traceheatmapchart/src/traceheatmap-chart-data-util.test.ts create mode 100644 traceheatmapchart/src/traceheatmap-chart-data-util.ts create mode 100644 traceheatmapchart/src/traceheatmap-chart-model.ts create mode 100644 traceheatmapchart/src/traceheatmap-duration-bucket-util.test.ts create mode 100644 traceheatmapchart/src/traceheatmap-duration-bucket-util.ts create mode 100644 traceheatmapchart/src/traceheatmap-time-bucket-util.test.ts create mode 100644 traceheatmapchart/src/traceheatmap-time-bucket-util.ts create mode 100644 traceheatmapchart/src/traceheatmap-tooltip-util.test.ts create mode 100644 traceheatmapchart/src/traceheatmap-tooltip-util.ts create mode 100644 traceheatmapchart/tsconfig.build.json create mode 100644 traceheatmapchart/tsconfig.json diff --git a/package-lock.json b/package-lock.json index 6d4ce1231..dc19b9c95 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "tracetable", "tracingganttchart", "victorialogs", + "traceheatmapchart", "e2e" ], "devDependencies": { @@ -4050,6 +4051,10 @@ "resolved": "victorialogs", "link": true }, + "node_modules/@perses/traceheatmap-chart-plugin": { + "resolved": "traceheatmapchart", + "link": true + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -17646,6 +17651,28 @@ "use-resize-observer": "^9.0.0" } }, + "traceheatmapchart": { + "name": "@perses/traceheatmap-chart-plugin", + "version": "0.1.0", + "license": "Apache-2.0", + "peerDependencies": { + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", + "@hookform/resolvers": "^3.2.0", + "@perses-dev/components": "^0.54.0-beta.11", + "@perses-dev/dashboards": "^0.54.0-beta.11", + "@perses-dev/plugin-system": "^0.54.0-beta.11", + "@perses-dev/spec": "^0.2.0-beta.6", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "echarts": "5.5.0", + "immer": "^10.1.1", + "lodash": "^4.17.21", + "react": "^17.0.2 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0", + "use-resize-observer": "^9.0.0" + } + }, "tracetable": { "name": "@perses-dev/trace-table-plugin", "version": "0.11.0-rc.1", diff --git a/package.json b/package.json index 02e8a3712..cbba4de90 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "tracetable", "tracingganttchart", "victorialogs", + "traceheatmapchart", "e2e" ], "peerDependencies": { diff --git a/scripts/npm/npm.go b/scripts/npm/npm.go index 80f3f6ea2..9945fd162 100644 --- a/scripts/npm/npm.go +++ b/scripts/npm/npm.go @@ -20,7 +20,7 @@ import ( ) func MustGetWorkspaces(dirPath string) []string { - excludedWorkspaces := []string{"e2e"} + excludedWorkspaces := []string{"e2e", "traceheatmapchart"} workspaces := npm.MustGetWorkspaces(dirPath) return slices.DeleteFunc(workspaces, func(w string) bool { return slices.Contains(excludedWorkspaces, w) diff --git a/traceheatmapchart/.cjs.swcrc b/traceheatmapchart/.cjs.swcrc new file mode 100644 index 000000000..2ed65083d --- /dev/null +++ b/traceheatmapchart/.cjs.swcrc @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": true + }, + "target": "es2022", + "transform": { + "react": { + "runtime": "automatic", + "useBuiltins": true + } + } + }, + "module": { + "type": "commonjs" + }, + "exclude": ["\\.(stories|test)\\."] +} diff --git a/traceheatmapchart/.eslintrc.js b/traceheatmapchart/.eslintrc.js new file mode 100644 index 000000000..fee7c61da --- /dev/null +++ b/traceheatmapchart/.eslintrc.js @@ -0,0 +1,78 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module.exports = { + extends: [ + 'eslint:recommended', + 'plugin:react/recommended', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + + plugins: ['import'], + + env: { + commonjs: true, + es6: true, + jest: true, + node: true, + browser: true, + }, + + parser: '@typescript-eslint/parser', + + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + + settings: { + react: { + version: 'detect', + }, + }, + + rules: { + 'prettier/prettier': 'error', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/array-type': [ + 'error', + { + default: 'array-simple', + }, + ], + 'import/order': 'error', + // you must disable the base rule as it can report incorrect errors + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + + 'react/prop-types': 'off', + 'react-hooks/exhaustive-deps': 'error', + // Not necessary in React 17 + 'react/react-in-jsx-scope': 'off', + 'react/jsx-curly-brace-presence': ['error', { props: 'never', children: 'never', propElementValues: 'always' }], + + // We use this rule instead of the core eslint `no-duplicate-imports` + // because it avoids false errors on cases where we have a regular + // import and an `import type`. + 'import/no-duplicates': 'error', + }, + + ignorePatterns: ['**/dist'], +}; diff --git a/traceheatmapchart/.gitignore b/traceheatmapchart/.gitignore new file mode 100644 index 000000000..fe8baa8ba --- /dev/null +++ b/traceheatmapchart/.gitignore @@ -0,0 +1,21 @@ +.idea/ + +# Local +.DS_Store +*.local +*.log* + +# Dist +node_modules +dist/ + +# IDE +.vscode/* +!.vscode/extensions.json +.idea + +# generated archives +*.tar.gz + +# external CUE dependencies +/*/cue.mod/pkg/ diff --git a/traceheatmapchart/.swcrc b/traceheatmapchart/.swcrc new file mode 100644 index 000000000..feaf67637 --- /dev/null +++ b/traceheatmapchart/.swcrc @@ -0,0 +1,21 @@ +{ + "$schema": "https://json.schemastore.org/swcrc", + "jsc": { + "parser": { + "syntax": "typescript", + "tsx": true + }, + "target": "es2022", + "transform": { + "react": { + "runtime": "automatic", + "useBuiltins": true + } + } + }, + "module": { + "type": "es6" + }, + "sourceMaps": true, + "exclude": ["\\.(stories|test)\\."] +} diff --git a/traceheatmapchart/LICENSE b/traceheatmapchart/LICENSE new file mode 100644 index 000000000..600b5758b --- /dev/null +++ b/traceheatmapchart/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +perses + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/traceheatmapchart/README.md b/traceheatmapchart/README.md new file mode 100644 index 000000000..d23afdec4 --- /dev/null +++ b/traceheatmapchart/README.md @@ -0,0 +1,43 @@ +# Plugin Module: traceheatmapchart + +### How to install + +This plugin requires react and react-dom 18 + +Install peer dependencies: + +```bash +npm install react@18 react-dom@18 +``` + +Install the plugin: + +```bash +npm install @my-org/traceheatmapchart +``` + +The Perses UI packages your plugin depends on are now maintained in the [`perses/shared`](https://github.com/perses/shared) repository. If you need to develop against local copies of those packages, follow the linking instructions in that repo. + +## Development + +### Setup + +Install dependencies: + +```bash +npm install +``` + +### Get Started + +Start the dev server: + +```bash +npm run dev +``` + +Build the plugin for distribution: + +```bash +npm run build +``` diff --git a/traceheatmapchart/cue.mod/module.cue b/traceheatmapchart/cue.mod/module.cue new file mode 100644 index 000000000..c82328f63 --- /dev/null +++ b/traceheatmapchart/cue.mod/module.cue @@ -0,0 +1,17 @@ +module: "github.com/perses/traceheatmapchart@v0" +language: { + version: "v0.15.1" +} +source: { + kind: "git" +} +deps: { + "github.com/perses/perses/cue@v0": { + v: "v0.53.0" + default: true + } + "github.com/perses/shared/cue@v0": { + v: "v0.53.1" + default: true + } +} diff --git a/traceheatmapchart/go.mod b/traceheatmapchart/go.mod new file mode 100644 index 000000000..2d646b5e8 --- /dev/null +++ b/traceheatmapchart/go.mod @@ -0,0 +1,30 @@ +module github.com/perses/plugins/prometheus + +go 1.23.4 + +require github.com/perses/perses v0.50.1 + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.4 // indirect + github.com/jpillora/backoff v1.0.0 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/muhlemmer/gu v0.3.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f // indirect + github.com/prometheus/client_golang v1.20.5 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.61.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect + github.com/zitadel/oidc/v3 v3.33.1 // indirect + github.com/zitadel/schema v1.3.0 // indirect + golang.org/x/crypto v0.37.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sys v0.32.0 // indirect + golang.org/x/text v0.24.0 // indirect + google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/traceheatmapchart/go.sum b/traceheatmapchart/go.sum new file mode 100644 index 000000000..4e95fbdaf --- /dev/null +++ b/traceheatmapchart/go.sum @@ -0,0 +1,66 @@ +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= +github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= +github.com/muhlemmer/gu v0.3.1/go.mod h1:YHtHR+gxM+bKEIIs7Hmi9sPT3ZDUvTN/i88wQpZkrdM= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nexucis/lamenv v0.5.2 h1:tK/u3XGhCq9qIoVNcXsK9LZb8fKopm0A5weqSRvHd7M= +github.com/nexucis/lamenv v0.5.2/go.mod h1:HusJm6ltmmT7FMG8A750mOLuME6SHCsr2iFYxp5fFi0= +github.com/perses/perses v0.50.1 h1:ySqFYu+/WXVWpDfSBZISN49m5docbrnNvjom7Ym9iNg= +github.com/perses/perses v0.50.1/go.mod h1:L6bykOUCMAI6CzGSMpK5EWT9ghBUHKxRP91LDCPpXW4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= +github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/zitadel/oidc/v3 v3.33.1 h1:e3w9PDV0Mh50/ZiJWtzyT0E4uxJ6RXll+hqVDnqGbTU= +github.com/zitadel/oidc/v3 v3.33.1/go.mod h1:zkoZ1Oq6CweX3BaLrftLEGCs6YK6zDpjjVGZrP10AWU= +github.com/zitadel/schema v1.3.0 h1:kQ9W9tvIwZICCKWcMvCEweXET1OcOyGEuFbHs4o5kg0= +github.com/zitadel/schema v1.3.0/go.mod h1:NptN6mkBDFvERUCvZHlvWmmME+gmZ44xzwRXwhzsbtc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/traceheatmapchart/jest.config.ts b/traceheatmapchart/jest.config.ts new file mode 100644 index 000000000..5190fb4de --- /dev/null +++ b/traceheatmapchart/jest.config.ts @@ -0,0 +1,23 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { Config } from '@jest/types'; +import shared from '../jest.shared'; + +const jestConfig: Config.InitialOptions = { + ...shared, + + setupFilesAfterEnv: [...(shared.setupFilesAfterEnv ?? []), '/src/setup-tests.ts'], +}; + +export default jestConfig; diff --git a/traceheatmapchart/package.json b/traceheatmapchart/package.json new file mode 100644 index 000000000..ce4b45e56 --- /dev/null +++ b/traceheatmapchart/package.json @@ -0,0 +1,63 @@ +{ + "name": "@perses/traceheatmap-chart-plugin", + "version": "0.1.0", + "license": "Apache-2.0", + "homepage": "https://github.com/perses/plugins/blob/main/README.md", + "repository": { + "type": "git", + "url": "git+https://github.com/perses/plugins.git" + }, + "bugs": { + "url": "https://github.com/perses/plugins/issues" + }, + "scripts": { + "dev": "rsbuild dev", + "build": "npm run build-mf && concurrently \"npm:build:*\"", + "build-mf": "rsbuild build", + "build:cjs": "swc ./src -d dist/lib/cjs --strip-leading-paths --config-file .cjs.swcrc", + "build:esm": "swc ./src -d dist/lib --strip-leading-paths --config-file .swcrc", + "build:types": "tsc --project tsconfig.build.json", + "lint": "eslint src --ext .ts,.tsx", + "test": "cross-env LC_ALL=C TZ=UTC jest", + "type-check": "tsc --noEmit" + }, + "main": "lib/cjs/index.js", + "module": "lib/index.js", + "types": "lib/index.d.ts", + "peerDependencies": { + "@emotion/react": "^11.7.1", + "@emotion/styled": "^11.6.0", + "@hookform/resolvers": "^3.2.0", + "@perses-dev/components": "^0.54.0-beta.11", + "@perses-dev/dashboards": "^0.54.0-beta.11", + "@perses-dev/plugin-system": "^0.54.0-beta.11", + "@perses-dev/spec": "^0.2.0-beta.6", + "date-fns": "^4.1.0", + "date-fns-tz": "^3.2.0", + "echarts": "5.5.0", + "lodash": "^4.17.21", + "react": "^17.0.2 || ^18.0.0", + "react-dom": "^17.0.2 || ^18.0.0", + "use-resize-observer": "^9.0.0", + "immer": "^10.1.1" + }, + "files": [ + "lib/**/*", + "__mf/**/*", + "mf-manifest.json", + "mf-stats.json" + ], + "perses": { + "plugins": [ + { + "kind": "Panel", + "spec": { + "display": { + "name": "Traceheatmap Chart" + }, + "name": "Traceheatmapchart" + } + } + ] + } +} diff --git a/traceheatmapchart/rsbuild.config.ts b/traceheatmapchart/rsbuild.config.ts new file mode 100644 index 000000000..7984dc2fa --- /dev/null +++ b/traceheatmapchart/rsbuild.config.ts @@ -0,0 +1,42 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { pluginReact } from '@rsbuild/plugin-react'; +import { createConfigForPlugin } from '../rsbuild.shared'; + +export default createConfigForPlugin({ + name: 'Traceheatmapchart', + rsbuildConfig: { + server: { port: 3023 }, + plugins: [pluginReact()], + }, + moduleFederation: { + exposes: { + './Traceheatmapchart': './src/TraceheatmapChart.ts', + }, + shared: { + react: { requiredVersion: '18.2.0', singleton: true }, + 'react-dom': { requiredVersion: '18.2.0', singleton: true }, + echarts: { singleton: true }, + 'date-fns': { singleton: true }, + 'date-fns-tz': { singleton: true }, + lodash: { singleton: true }, + '@perses-dev/components': { singleton: true }, + '@perses-dev/dashboards': { singleton: true }, + '@perses-dev/plugin-system': { singleton: true }, + '@emotion/react': { requiredVersion: '^11.11.3', singleton: true }, + '@emotion/styled': { singleton: true }, + '@hookform/resolvers': { singleton: true }, + }, + }, +}); diff --git a/traceheatmapchart/schemas/traceheatmapchart.cue b/traceheatmapchart/schemas/traceheatmapchart.cue new file mode 100644 index 000000000..3ff425fdc --- /dev/null +++ b/traceheatmapchart/schemas/traceheatmapchart.cue @@ -0,0 +1,24 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package model + +kind: "Traceheatmapchart" + +spec: close({ + bucketSettings: #bucketSettings +}) + +#bucketSettings: { + base: *2 | 10 +} diff --git a/traceheatmapchart/src/TraceheatmapChart.ts b/traceheatmapchart/src/TraceheatmapChart.ts new file mode 100644 index 000000000..3943df32f --- /dev/null +++ b/traceheatmapchart/src/TraceheatmapChart.ts @@ -0,0 +1,25 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { PanelPlugin } from '@perses-dev/plugin-system'; +import { createInitialTraceheatmapChartOptions, TraceheatmapOptions } from './traceheatmap-chart-model'; +import { TraceheatmapSettingsEditor } from './TraceheatmapSettingsEditor'; + +import { TraceheatmapChartPanel, TraceheatmapChartProps } from './TraceheatmapChartPanel'; + +export const Traceheatmapchart: PanelPlugin = { + PanelComponent: TraceheatmapChartPanel, + supportedQueryTypes: ['TraceQuery'], + panelOptionsEditorComponents: [{ label: 'General Settings', content: TraceheatmapSettingsEditor }], + createInitialOptions: createInitialTraceheatmapChartOptions, +}; diff --git a/traceheatmapchart/src/TraceheatmapChartPanel.tsx b/traceheatmapchart/src/TraceheatmapChartPanel.tsx new file mode 100644 index 000000000..d4ea32ff2 --- /dev/null +++ b/traceheatmapchart/src/TraceheatmapChartPanel.tsx @@ -0,0 +1,146 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { PanelProps } from '@perses-dev/plugin-system'; +import { ReactElement, useMemo, useRef } from 'react'; +import { TraceData } from '@perses-dev/spec'; +import { EChart, useChartsContext } from '@perses-dev/components'; +import { ECharts as EChartsInstance } from 'echarts/core'; +import { EChartsOption } from 'echarts'; +import { Box, Typography } from '@mui/material'; +import { TraceheatmapOptions } from './traceheatmap-chart-model'; +import { FlatQueryResults, getHeatmapChartData, getPlotterData } from './traceheatmap-chart-data-util'; +import { createBucketToolTip } from './traceheatmap-tooltip-util'; + +export type TraceheatmapChartProps = PanelProps; + +export const TraceheatmapChartPanel = (props: TraceheatmapChartProps): ReactElement => { + const { + queryResults, + spec: { + bucketSettings: { base }, + }, + } = props; + + const chartRef = useRef(); + const { chartsTheme } = useChartsContext(); + + const allFlatResults = useMemo(() => { + const allFlatResults: FlatQueryResults = []; + queryResults.forEach((qr) => { + (qr.data.searchResult || []).forEach((sr) => { + allFlatResults.push({ ...sr }); + }); + }); + return allFlatResults; + }, [queryResults]); + + const { bucketCountMap, bucketToFlatResultsMap, durationBuckets, maxBucketCount, minBucketCount, timeBuckets } = + useMemo(() => { + return getHeatmapChartData(allFlatResults, base); + }, [allFlatResults, base]); + + const data = useMemo(() => { + return getPlotterData(bucketCountMap, { showZero: false, zeroSubstitute: ' ' }); + }, [bucketCountMap]); + + if ( + [bucketCountMap, bucketToFlatResultsMap, timeBuckets, durationBuckets].some((i) => i === undefined || !i.length) + ) { + return ( + + No data + + ); + } + + const option: EChartsOption = { + tooltip: { + appendTo: 'body', + trigger: 'item', + confine: false, + formatter: (params) => { + return createBucketToolTip(params, { + bucketCountMap, + bucketToFlatResultsMap, + durationBuckets, + timeBuckets, + flatResults: allFlatResults, + }); + }, + }, + grid: { + top: 10, + right: 10, + bottom: 20, + left: 20, + }, + xAxis: { + type: 'category', + data: timeBuckets.map((tb) => tb.label), + splitArea: { + show: true, + }, + }, + yAxis: { + type: 'category', + data: durationBuckets.map((db) => db.label), + splitArea: { + show: true, + }, + }, + visualMap: { + min: minBucketCount, + max: maxBucketCount, + calculable: true, + show: false, + }, + series: [ + { + name: 'Traces', + type: 'heatmap', + data: data, + label: { + show: true, + }, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowColor: 'rgba(0, 0, 0, 0.5)', + }, + }, + }, + ], + }; + + return ( + + + + ); +}; diff --git a/traceheatmapchart/src/TraceheatmapSettingsEditor.tsx b/traceheatmapchart/src/TraceheatmapSettingsEditor.tsx new file mode 100644 index 000000000..eada07fba --- /dev/null +++ b/traceheatmapchart/src/TraceheatmapSettingsEditor.tsx @@ -0,0 +1,63 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { OptionsEditorProps } from '@perses-dev/plugin-system'; +import { ReactElement, useCallback } from 'react'; +import { + OptionsEditorColumn, + OptionsEditorControl, + OptionsEditorGrid, + OptionsEditorGroup, +} from '@perses-dev/components'; +import { FormControl, InputLabel, MenuItem, Select, SelectChangeEvent } from '@mui/material'; +import { TraceheatmapOptions } from './traceheatmap-chart-model'; +import { ExponentialBase } from './traceheatmap-duration-bucket-util'; + +export type TraceheatmapSettingsEditorProps = OptionsEditorProps; + +export const TraceheatmapSettingsEditor = (props: TraceheatmapSettingsEditorProps): ReactElement => { + const { onChange, value } = props; + const bases: ExponentialBase[] = [2, 10]; + + const handleChange = useCallback( + (event: SelectChangeEvent) => { + const base = Number(event.target.value) as ExponentialBase; + onChange({ ...value, bucketSettings: { ...value.bucketSettings, base } }); + }, + [onChange, value] + ); + + return ( + + + + + Base + + + } + /> + + + + ); +}; diff --git a/traceheatmapchart/src/bootstrap.tsx b/traceheatmapchart/src/bootstrap.tsx new file mode 100644 index 000000000..06f1ffc5a --- /dev/null +++ b/traceheatmapchart/src/bootstrap.tsx @@ -0,0 +1,18 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import React from 'react'; +import ReactDOM from 'react-dom/client'; + +const root = ReactDOM.createRoot(document.getElementById('root')!); +root.render(); diff --git a/traceheatmapchart/src/env.d.ts b/traceheatmapchart/src/env.d.ts new file mode 100644 index 000000000..01f4e7302 --- /dev/null +++ b/traceheatmapchart/src/env.d.ts @@ -0,0 +1,14 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/// diff --git a/traceheatmapchart/src/getPluginModule.ts b/traceheatmapchart/src/getPluginModule.ts new file mode 100644 index 000000000..cab8b384b --- /dev/null +++ b/traceheatmapchart/src/getPluginModule.ts @@ -0,0 +1,30 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { PluginModuleResource, PluginModuleSpec } from '@perses-dev/plugin-system'; +import packageJson from '../package.json'; + +/** + * Returns the plugin module information from package.json + */ +export function getPluginModule(): PluginModuleResource { + const { name, version, perses } = packageJson; + return { + kind: 'PluginModule', + metadata: { + name, + version, + }, + spec: perses as PluginModuleSpec, + }; +} diff --git a/traceheatmapchart/src/index-federation.ts b/traceheatmapchart/src/index-federation.ts new file mode 100644 index 000000000..df7f0bd82 --- /dev/null +++ b/traceheatmapchart/src/index-federation.ts @@ -0,0 +1,14 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import('./bootstrap'); diff --git a/traceheatmapchart/src/index.ts b/traceheatmapchart/src/index.ts new file mode 100644 index 000000000..8190dcdd3 --- /dev/null +++ b/traceheatmapchart/src/index.ts @@ -0,0 +1,14 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export { getPluginModule } from './getPluginModule'; diff --git a/traceheatmapchart/src/setup-tests.ts b/traceheatmapchart/src/setup-tests.ts new file mode 100644 index 000000000..d43de32e0 --- /dev/null +++ b/traceheatmapchart/src/setup-tests.ts @@ -0,0 +1,17 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import '@testing-library/jest-dom'; + +// Always mock e-charts during tests since we don't have a proper canvas in jsdom +jest.mock('echarts/core'); diff --git a/traceheatmapchart/src/trace-search-result-mock.ts b/traceheatmapchart/src/trace-search-result-mock.ts new file mode 100644 index 000000000..9d60dcc8a --- /dev/null +++ b/traceheatmapchart/src/trace-search-result-mock.ts @@ -0,0 +1,815 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export const traceMock = [ + { + startTimeUnixMs: 1784192174685.3193, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192174654.7686, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192172653.8154, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192172642.9243, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192171742.1064, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192170841.6458, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192170690.9993, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192170660.736, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192169760.4045, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192169679.942, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192169649.6548, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192167648.3906, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192167497.7976, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192167486.031, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192166585.4404, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192166434.9548, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192166284.4407, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192166203.7334, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192166172.8992, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192165772.3674, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192165762.1357, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192164861.279, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192164460.4258, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192163559.9954, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192163409.605, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192162508.731, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192162107.858, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192161207.4866, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192161193.9077, + durationMs: 13, + }, + { + startTimeUnixMs: 1784192161163.3796, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192160262.685, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192159862.2695, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192159851.717, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192159451.1855, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192159050.4116, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192158649.8252, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192157749.162, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192152748.7302, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192151848.0571, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192151766.5947, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192151686.169, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192151675.7922, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192151665.0178, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192151654.413, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192151573.7412, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192149572.9575, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192148672.8562, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192148592.5503, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192148561.9368, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192147661.371, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192145661.0486, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192145630.3525, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192145479.727, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192145469.01, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192143468.5906, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192143458.0422, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192143428.0112, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192143417.49, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192143016.8562, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192142616.481, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192142534.5754, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192142504.277, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192142473.7092, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192142073.0256, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192140072.4106, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192140041.936, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192138040.5986, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192138009.2964, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192137858.8264, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192137848.1501, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192137447.6226, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192135446.7249, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192130446.2883, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192130045.8477, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192130015.036, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192128014.522, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192127934.071, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192122932.8218, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192122922.2854, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192122841.2324, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192120840.8254, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192120439.5728, + durationMs: 401, + }, + { + startTimeUnixMs: 1784192120355.559, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192118354.576, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192113354.0989, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192113343.55, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192111343.1006, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192111312.1196, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192111281.0774, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192111270.5344, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192111190.0708, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192111039.9849, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192109039.289, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192104038.8708, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192104028.1548, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192099027.4658, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192097026.6997, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192097016.131, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192096984.6975, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192096584.1492, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192096503.571, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192096352.4004, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192095451.448, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192090450.7217, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192088449.989, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192088439.3752, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192088407.0408, + durationMs: 32, + }, + { + startTimeUnixMs: 1784192088256.3264, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192086255.661, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192085855.1626, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192083854.3394, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192082953.9902, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192077953.517, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192077052.9263, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192076151.8887, + durationMs: 901, + }, + { + startTimeUnixMs: 1784192075250.7153, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192073249.3381, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192073098.1663, + durationMs: 151, + }, + { + startTimeUnixMs: 1784192072696.7864, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192072296.4514, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192072260.7258, + durationMs: 35, + }, + { + startTimeUnixMs: 1784192070259.9565, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192069359.8064, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192069329.0261, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192069318.2317, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192069287.2703, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192064286.857, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192062283.6328, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192062132.1172, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192062051.627, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192061647.1904, + durationMs: 403, + }, + { + startTimeUnixMs: 1784192059646.4438, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192059496.0369, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192059095.6033, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192059065.0618, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192058984.4597, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192056983.641, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192051983.162, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192051952.4688, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192051921.7188, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192049921.1357, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192049905.3608, + durationMs: 15, + }, + { + startTimeUnixMs: 1784192048998.6458, + durationMs: 906, + }, + { + startTimeUnixMs: 1784192048988.0898, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192046987.4468, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192046086.5837, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192045936.229, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192045925.6958, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192045915.179, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192045884.641, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192045853.962, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192044953.309, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192042952.8193, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192037952.4658, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192037941.9148, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192032941.4517, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192032040.8254, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192031960.1401, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192031949.7449, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192026948.759, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192026868.498, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192026838.063, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192024837.3977, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192024437.0999, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192024405.129, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192024394.2751, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192024313.8738, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192019313.7886, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784192018413.3171, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192018012.782, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192017111.7844, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192017081.5464, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192017071.0217, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192016169.9194, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192015267.4995, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192010266.0486, + durationMs: 5001, + }, + { + startTimeUnixMs: 1784192010115.4226, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192008115.1465, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192008104.5806, + durationMs: 10, + }, + { + startTimeUnixMs: 1784192007954.1602, + durationMs: 150, + }, + { + startTimeUnixMs: 1784192007923.279, + durationMs: 30, + }, + { + startTimeUnixMs: 1784192007022.5178, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192006122.1655, + durationMs: 900, + }, + { + startTimeUnixMs: 1784192005970.3157, + durationMs: 151, + }, + { + startTimeUnixMs: 1784192005889.768, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192005489.311, + durationMs: 400, + }, + { + startTimeUnixMs: 1784192005408.8762, + durationMs: 80, + }, + { + startTimeUnixMs: 1784192003408.443, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784192001408.0996, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784191996404.2163, + durationMs: 5001, + }, + { + startTimeUnixMs: 1784191996323.9153, + durationMs: 80, + }, + { + startTimeUnixMs: 1784191991323.4905, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784191991293.1313, + durationMs: 30, + }, + { + startTimeUnixMs: 1784191991262.6406, + durationMs: 30, + }, + { + startTimeUnixMs: 1784191986261.826, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784191981261.2673, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784191981230.6716, + durationMs: 30, + }, + { + startTimeUnixMs: 1784191981150.0059, + durationMs: 80, + }, + { + startTimeUnixMs: 1784191981139.4368, + durationMs: 10, + }, + { + startTimeUnixMs: 1784191979138.5269, + durationMs: 2000, + }, +]; diff --git a/traceheatmapchart/src/traceheatmap-chart-data-util.test.ts b/traceheatmapchart/src/traceheatmap-chart-data-util.test.ts new file mode 100644 index 000000000..2da02f847 --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-chart-data-util.test.ts @@ -0,0 +1,27 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { traceMock } from './trace-search-result-mock'; +import { FlatQueryResults, getHeatmapChartData } from './traceheatmap-chart-data-util'; + +describe('traceheatmap-chart-data-util', () => { + const { bucketCountMap, bucketToFlatResultsMap, durationBuckets, timeBuckets } = getHeatmapChartData( + traceMock as FlatQueryResults, + 2 + ); + test('cellWeightMap, metaDataArray, timeBuckets, durationBuckets should not be undefined', () => { + [bucketCountMap, bucketToFlatResultsMap, timeBuckets, durationBuckets].forEach((i) => { + expect(i).not.toBeUndefined(); + }); + }); +}); diff --git a/traceheatmapchart/src/traceheatmap-chart-data-util.ts b/traceheatmapchart/src/traceheatmap-chart-data-util.ts new file mode 100644 index 000000000..0a56fd1d0 --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-chart-data-util.ts @@ -0,0 +1,140 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { TraceSearchResult } from '@perses-dev/spec'; +import { ExponentialBase, getDurationBuckets, TraceHeatMapDurationBuckets } from './traceheatmap-duration-bucket-util'; +import { getTimeBuckets, TraceHeatMapTimeBucket } from './traceheatmap-time-bucket-util'; + +export type HeatmapChartData = { + bucketCountMap: number[][]; + bucketToFlatResultsMap: number[][][]; + timeBuckets: TraceHeatMapTimeBucket; + durationBuckets: TraceHeatMapDurationBuckets; + minBucketCount: number; + maxBucketCount: number; +}; + +export type PlotterOptions = { + showZero: boolean; + zeroSubstitute: string; +}; + +export type FlatQueryResults = TraceSearchResult[]; + +export const getHeatmapChartData = (flatQueryResults: FlatQueryResults, base: ExponentialBase): HeatmapChartData => { + /* e-chart heatmap uses the min and max for setting the color range from the most pale to darkest*/ + let minBucketCount = Infinity; + let maxBucketCount = -Infinity; + + /** + * Both buckets are generated according to the query results (traces) + * to avoid fixed length and sparse buckets + */ + const timeBuckets = getTimeBuckets( + flatQueryResults.map((r) => ({ durationMs: r.durationMs, startTimeUnixMs: r.startTimeUnixMs })) + ); + const durationBuckets = getDurationBuckets( + flatQueryResults.map((qst) => qst.durationMs), + base + ); + + /** + * initiate the bucket-count and flat-data-index maps + * the latter one is used to retrieve the details of query results from the received results + */ + const bucketCountMap: number[][] = []; + const bucketToFlatResultsMap: number[][][] = []; + + timeBuckets.forEach(() => { + const valueRow: number[] = []; + const metaDataMap: number[][] = []; + durationBuckets.forEach(() => { + valueRow.push(0); + metaDataMap.push([]); + }); + bucketCountMap.push(valueRow); + bucketToFlatResultsMap.push(metaDataMap); + }); + + /* find the row and column indexes to find the fit bucket for all records */ + for (let i = 0; i < flatQueryResults.length; i += 1) { + const { startTimeUnixMs, durationMs } = flatQueryResults[i]!; + let timeBucketIndex = 0; + let durationBucketIndex = 0; + + for (let j = 0; j < timeBuckets.length; j += 1) { + if (j === timeBuckets.length - 1) { + timeBucketIndex = j; + break; + } + if (startTimeUnixMs >= timeBuckets[j]!.startTimestamp && startTimeUnixMs < timeBuckets[j + 1]!.startTimestamp) { + timeBucketIndex = j; + break; + } + } + + for (let j = 0; j < durationBuckets.length; j += 1) { + if (j === durationBuckets.length - 1) { + durationBucketIndex = j; + break; + } + if (durationMs >= durationBuckets[j]!.start && durationMs < durationBuckets[j + 1]!.start) { + durationBucketIndex = j; + break; + } + } + + const bucketCount = bucketCountMap[timeBucketIndex]![durationBucketIndex]! + 1; + bucketCountMap[timeBucketIndex]![durationBucketIndex]! = bucketCount; + + if (bucketCount > maxBucketCount) { + maxBucketCount = bucketCount; + } + + if (bucketCount < minBucketCount) { + minBucketCount = bucketCount; + } + + bucketToFlatResultsMap[timeBucketIndex]![durationBucketIndex]!.push(i); + } + + return { + bucketCountMap, + bucketToFlatResultsMap, + timeBuckets, + durationBuckets, + minBucketCount, + maxBucketCount, + }; +}; + +/** + * prepares the appropriate e-chart heatmap data format + */ +export const getPlotterData = ( + cellWeightMap: number[][], + plotterOptions?: PlotterOptions +): Array> => { + const { showZero, zeroSubstitute } = plotterOptions || { showZero: false, zeroSubstitute: ' ' }; + const eChartData: Array> = []; + cellWeightMap.forEach((r, rIdx) => { + r.forEach((c, cIdx) => { + let value: string | number = c; + if (!showZero && !value) { + value = zeroSubstitute; + } + eChartData.push([rIdx, cIdx, value]); + }); + }); + return eChartData; +}; diff --git a/traceheatmapchart/src/traceheatmap-chart-model.ts b/traceheatmapchart/src/traceheatmap-chart-model.ts new file mode 100644 index 000000000..03a9f43b3 --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-chart-model.ts @@ -0,0 +1,26 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ExponentialBase } from './traceheatmap-duration-bucket-util'; + +export interface BucketSettings { + base: ExponentialBase; +} + +export interface TraceheatmapOptions { + bucketSettings: BucketSettings; +} + +export const createInitialTraceheatmapChartOptions = (): TraceheatmapOptions => ({ + bucketSettings: { base: 2 }, +}); diff --git a/traceheatmapchart/src/traceheatmap-duration-bucket-util.test.ts b/traceheatmapchart/src/traceheatmap-duration-bucket-util.test.ts new file mode 100644 index 000000000..001e2a887 --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-duration-bucket-util.test.ts @@ -0,0 +1,102 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ExponentialBase, getDurationBuckets } from './traceheatmap-duration-bucket-util'; + +type TestBody = { durations: number[]; base: ExponentialBase; desc: string; exp: string; testPurpose: string }; + +describe('traceheatmap-duration-bucket-util', () => { + describe('generate buckets', () => { + const tests: TestBody[] = [ + { + durations: [1, 2, 3, 4, 5, 6, 7, 8], + base: 2, + desc: 'a simple test for exponential buckets demonstration', + exp: '1,2,4,8', + testPurpose: 'demonstration base 2 - helps to understand bucketing', + }, + { + durations: [1, 2, 3, 4, 5, 6, 7, 8], + base: 10, + desc: 'a simple test for exponential buckets demonstration', + exp: '1,10', + testPurpose: 'demonstration base 10 - helps to understand bucketing', + }, + { + durations: [65, 51, 50, 46, 29, 37, 31], + base: 2, + desc: 'should range from 16 to +128', + exp: '16,32,64,128', + testPurpose: 'realistic network latency', + }, + { + durations: [65, 51, 1500, 46, 29, 37, 31], + base: 2, + desc: 'should range from 16 to +2048', + exp: '16,32,64,128,256,512,1024,2048', + testPurpose: 'realistic network latency with a considerable lag', + }, + { + durations: [65, 51, 1500, 46, 29, 37, 31], + base: 10, + desc: 'should range from 10 to +10000', + exp: '10,100,1000,10000', + testPurpose: 'realistic network latency with a considerable lag', + }, + { + durations: [0.5, 0.4, 1, 10, 2], + base: 2, + desc: 'should range from 0.25 to +16', + exp: '0.25,0.5,1,2,4,8,16', + testPurpose: 'minimum is microseconds 0 { + const { durations, base, exp, desc, testPurpose } = tst; + const buckets = getDurationBuckets(durations, base); + test(`base:${base}-${desc}-${testPurpose}`, () => { + const starts = buckets.map((b) => b.start).join(','); + expect(starts).toBe(exp); + }); + + test('buckets should be sorted', () => { + let sorted = true; + for (let i = 0; i < buckets.length; i += 1) { + if (i === buckets.length) { + break; + } + if (buckets[i]! > buckets[i + 1]!) { + sorted = false; + break; + } + } + expect(sorted).toBe(true); + }); + }); + }); +}); diff --git a/traceheatmapchart/src/traceheatmap-duration-bucket-util.ts b/traceheatmapchart/src/traceheatmap-duration-bucket-util.ts new file mode 100644 index 000000000..6bac9d157 --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-duration-bucket-util.ts @@ -0,0 +1,47 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* shortened */ +const max = Math.max; +const min = Math.min; +const floor = Math.floor; +const ceil = Math.ceil; +const pow = Math.pow; + +/** + * From the mathematical perspective these are the two most common bases which could be used for exponential functions, though any number would be technically valid + * Both come with advantages and disadvantages. The base should be picked by the users accordingly. + * For instance, base 2 may be appropriate for the network latency, whereas 10 is much better for db operations + */ +export type ExponentialBase = 2 | 10; + +export type TraceHeatMapDurationBuckets = Array<{ start: number; label: string }>; + +const baseLog = (base: number, x: number) => Math.log(x) / Math.log(base); + +export const getDurationBuckets = (durations: number[], base: ExponentialBase = 2): TraceHeatMapDurationBuckets => { + const cleanDuration = durations.filter((d) => d > 0); + const minDuration = min(...cleanDuration); + const maxDuration = max(...cleanDuration); + const buckets: TraceHeatMapDurationBuckets = []; + + const startExponent = floor(baseLog(base, minDuration)); + const endExponent = ceil(baseLog(base, maxDuration)); + + for (let exp = startExponent; exp <= endExponent; exp++) { + const start = pow(base, exp); + const label = start < 1 ? `${start.toFixed(2)} ms` : `${start} ms`; + buckets.push({ start, label }); + } + return buckets; +}; diff --git a/traceheatmapchart/src/traceheatmap-time-bucket-util.test.ts b/traceheatmapchart/src/traceheatmap-time-bucket-util.test.ts new file mode 100644 index 000000000..f61bab3fd --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-time-bucket-util.test.ts @@ -0,0 +1,186 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { findScale, MILLISECONDS_TIME_SCALE } from './traceheatmap-time-bucket-util'; + +const formatDate = (date: number) => new Date(date).toISOString().split('T')[0]; +type TestBody = { start: number; end: number; desc: string; testPurpose: string; exp: string }; + +describe('traceheatmap-time-bucket-util', () => { + describe('scaling', () => { + describe('buckets', () => { + const tests: TestBody[] = [ + { + start: 1725896153082, + end: 1783956953082, + desc: 'should include 2024, 2025', + testPurpose: 'yearly scaled', + exp: '2024-2025,2025-2026', + }, + { + start: 1814400000000, + end: 1817078400000, + desc: 'should include July 2027', + testPurpose: 'monthly scaled', + exp: 'July', + }, + { + start: 1782864000000, + end: 1786492800000, + desc: 'should include July and August 2026', + testPurpose: 'stretched monthly scaled', + exp: 'July,August', + }, + { + start: 1782864000000, + end: 1789171200000, + desc: 'should include July, August, and September', + testPurpose: 'stretched monthly scaled', + exp: 'July,August,September', + }, + { + start: 1783987200000, + end: 1784592000000, + desc: 'should include 14th to 21st of July 2026', + testPurpose: 'weekly scaled', + exp: 'July 14 - July 21', + }, + { + start: 1783987200000, + end: 1784851200000, + desc: 'should include 14th to 28th of July 2026', + testPurpose: 'weekly scaled', + exp: 'July 14 - July 21,July 21 - July 28', + }, + { + start: 1784937600000, + end: 1785715200000, + desc: 'should include July 25th to 8th Aug 2026', + testPurpose: 'stretched weekly scaled', + exp: 'July 25 - August 1,August 1 - August 8', + }, + { + start: 1782864000000, + end: 1785456000000, + desc: 'should include the entire July 2026 and the beginning of August', + testPurpose: 'entire month - weekly scaled', + exp: 'July 1 - July 8,July 8 - July 15,July 15 - July 22,July 22 - July 29,July 29 - August 5', + }, + { + start: 1769904000000, + end: 1772236800000, + desc: 'should include the entire Feb (28 days) 2026', + testPurpose: 'entire Feb (28 days) - weekly scaled', + exp: 'February 1 - February 8,February 8 - February 15,February 15 - February 22,February 22 - March 1', + }, + { + start: 1706745600000, + end: 1709164800000, + desc: 'should include the entire Feb (29 days - leap year) 2024', + testPurpose: 'entire Feb (29 days) - weekly scaled', + exp: 'February 1 - February 8,February 8 - February 15,February 15 - February 22,February 22 - February 29', + }, + { + start: 1783987200000, + end: 1784091600000, + desc: 'should include week days', + testPurpose: 'daily scaled', + exp: 'Tuesday,Wednesday', + }, + { + start: 1783990800000, + end: 1784037600000, + desc: 'should include hours', + testPurpose: 'hourly scaled', + exp: '1 AM - 2 AM,2 AM - 3 AM,3 AM - 4 AM,4 AM - 5 AM,5 AM - 6 AM,6 AM - 7 AM,7 AM - 8 AM,8 AM - 9 AM,9 AM - 10 AM,10 AM - 11 AM,11 AM - 12 PM,12 PM - 1 PM,1 PM - 2 PM', + }, + { + start: 1784640900000, + end: 1784648220000, + desc: 'should include hours', + testPurpose: 'hourly scaled', + exp: '1 PM - 2 PM,2 PM - 3 PM,3 PM - 4 PM', + }, + { + start: 1784640900000, + end: 1784648700000, + desc: 'should include hours', + testPurpose: 'hourly scaled', + exp: '1 PM - 2 PM,2 PM - 3 PM,3 PM - 4 PM', + }, + { + start: 1783990800000, + end: 1784073599000, + desc: 'should include the entire hours of the day', + testPurpose: 'hourly scaled', + exp: '1 AM - 2 AM,2 AM - 3 AM,3 AM - 4 AM,4 AM - 5 AM,5 AM - 6 AM,6 AM - 7 AM,7 AM - 8 AM,8 AM - 9 AM,9 AM - 10 AM,10 AM - 11 AM,11 AM - 12 PM,12 PM - 1 PM,1 PM - 2 PM,2 PM - 3 PM,3 PM - 4 PM,4 PM - 5 PM,5 PM - 6 PM,6 PM - 7 PM,7 PM - 8 PM,8 PM - 9 PM,9 PM - 10 PM,10 PM - 11 PM,11 PM - 12 AM', + }, + { + start: 1784066400000, + end: 1784088000000, + desc: 'should include hours of 14th and 15th of July 2026', + testPurpose: 'stretched hourly scaled', + exp: '10 PM - 11 PM,11 PM - 12 AM,12 AM - 1 AM,1 AM - 2 AM,2 AM - 3 AM,3 AM - 4 AM', + }, + { + start: 1784088060000, + end: 1784088300000, + desc: 'should include minutes', + testPurpose: 'minutes scaled', + exp: '04:01-04:02,04:02-04:03,04:03-04:04,04:04-04:05', + }, + { + start: 1784159880000, + end: 1784160300000, + desc: 'should include minutes', + testPurpose: 'stretched minutes scaled', + exp: '23:58-23:59,23:59-00:00,00:00-00:01,00:01-00:02,00:02-00:03,00:03-00:04,00:04-00:05', + }, + { + start: 1784649599000, + end: 1784649605000, + desc: 'should include seconds', + testPurpose: 'seconds scaled', + exp: '59-0,0-1,1-2,2-3,3-4,4-5', + }, + ]; + + tests.forEach((tst) => { + const { start, end, exp, testPurpose } = tst; + + const { getBuckets } = MILLISECONDS_TIME_SCALE.find((i) => i.scale === findScale(start, end))!; + const buckets = getBuckets(start, end); + + test(`from ${formatDate(start)} to ${formatDate(end)}: ${tst.desc} demonstrates: ${testPurpose}`, () => { + const labels = buckets.map((i) => i.label).join(','); + expect(labels).toBe(exp); + }); + + test(`generated buckets ranging from ${buckets[0]?.startTimestamp} to ${buckets[buckets.length - 1]?.startTimestamp} should be sorted ascendingly`, () => { + const startTimestamps = buckets.map((i) => i.startTimestamp); + let sortedBuckets = true; + for (let i = 0; i < startTimestamps.length; i += 1) { + if (i === startTimestamps.length - 1) { + break; + } + if (startTimestamps[i]! > startTimestamps[i + 1]!) { + sortedBuckets = false; + break; + } + } + expect(sortedBuckets).toBe(true); + }); + }); + }); + }); +}); diff --git a/traceheatmapchart/src/traceheatmap-time-bucket-util.ts b/traceheatmapchart/src/traceheatmap-time-bucket-util.ts new file mode 100644 index 000000000..1940302bf --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-time-bucket-util.ts @@ -0,0 +1,220 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { TraceSearchResult } from '@perses-dev/spec'; + +/* shortened */ +const trunc = Math.trunc; +const ciel = Math.ceil; +const max = Math.max; +const min = Math.min; + +export type TraceHeatMapTimeBucket = Array<{ label: string; startTimestamp: number }>; + +export type Scale = 'yearly' | 'monthly' | 'weekly' | 'daily' | 'hourly' | 'minutes' | 'seconds'; + +export type TraceHeatMapTimeScale = { + scale: Scale; + getBuckets: (start: number, end: number, local?: string) => TraceHeatMapTimeBucket; +}; + +export const TIME_IN_MS = { + get second() { + return 1000; + }, + get minute() { + return 60 * this.second; + }, + get hour() { + return 60 * this.minute; + }, + get day() { + return 24 * this.hour; + }, + get week() { + return 7 * this.day; + }, + get month() { + /** + * A month may include 28 to 31 days. + * To ease the scaling process and generate buckets, we keep the maximum which is 31 days. + * Therefore, months with less than 31 days simply fall under the weekly scaling and produce weekly buckets + */ + return 31 * this.day; + }, + get year() { + /** + * Any value greater than equal the year definition is scaled yearly. + * This includes the leap years which have 366 days. + */ + return 365 * this.day; + }, +}; + +export const findScale = (start: number, end: number): Scale | undefined => { + let scale: Scale | undefined = undefined; + const diff = end - start; + + if (diff <= 0) return undefined; + + if (diff >= TIME_IN_MS.year) { + scale = 'yearly'; + } else if (diff >= TIME_IN_MS.month) { + scale = 'monthly'; + } else if (diff >= TIME_IN_MS.week) { + scale = 'weekly'; + } else if (diff >= TIME_IN_MS.day) { + scale = 'daily'; + } else if (diff >= TIME_IN_MS.hour) { + scale = 'hourly'; + } else if (diff >= TIME_IN_MS.minute) { + scale = 'minutes'; + } else if (diff > 0) { + scale = 'seconds'; + } + return scale; +}; + +type LabelGenerator = ((ts: number, local?: string) => string) | ((ts: number, local?: string) => () => string); + +const produceBuckets = ( + scale: keyof typeof TIME_IN_MS, + labelGenerator: LabelGenerator, + start: number, + end: number, + local?: string +): TraceHeatMapTimeBucket => { + const diff = trunc(end) - trunc(start); + const bucketCounts = ciel(diff / TIME_IN_MS[scale]); + const firstLabel = labelGenerator(start, local); + + const buckets: TraceHeatMapTimeBucket = [ + { startTimestamp: start, label: typeof firstLabel === 'function' ? firstLabel() : firstLabel }, + ]; + + for (let i = 1; i < bucketCounts; i += 1) { + const lastItem = buckets[buckets.length - 1]; + if (!lastItem) break; + const nextStartTimeStamp = lastItem.startTimestamp + TIME_IN_MS[scale]; + const lbl = labelGenerator(nextStartTimeStamp, local); + buckets.push({ startTimestamp: nextStartTimeStamp, label: typeof lbl === 'function' ? lbl() : lbl }); + } + + return buckets; +}; + +export const MILLISECONDS_TIME_SCALE: TraceHeatMapTimeScale[] = [ + { + scale: 'yearly', + getBuckets: (start, end, local) => { + const generateYearlyLabel = (ts: number) => { + const begin = new Date(ts).toISOString().split('T')[0]?.split('-')[0] ?? ''; + const end = Number(begin) + 1; + return `${begin}-${end}`; + }; + return produceBuckets('year', generateYearlyLabel, start, end, local); + }, + }, + { + scale: 'monthly', + getBuckets: (start, end, local) => { + /* optimized using a js closure to avoid creating Intl.DateTimeFormat in every iteration */ + const generateMonthlyLabel = (ts: number) => { + const formatter = new Intl.DateTimeFormat(local ?? 'en-US', { month: 'long' }); + return () => { + return formatter.format(ts); + }; + }; + return produceBuckets('month', generateMonthlyLabel, start, end, local); + }, + }, + { + scale: 'weekly', + getBuckets: (start, end, local) => { + /* optimized using a js closure to avoid creating Intl.DateTimeFormat in every iteration */ + const generateWeeklyLabel = (ts: number) => { + const formatter = new Intl.DateTimeFormat(local ?? 'en-US', { day: 'numeric', month: 'long' }); + return () => { + return `${formatter.format(ts)} - ${formatter.format(ts + TIME_IN_MS.week)}`; + }; + }; + return produceBuckets('week', generateWeeklyLabel, start, end, local); + }, + }, + { + scale: 'daily', + getBuckets: (start, end, local) => { + const generateDailyLabels = (ts: number) => { + const formatter = new Intl.DateTimeFormat(local ?? 'en-US', { weekday: 'long' }); + return () => { + return formatter.format(ts); + }; + }; + return produceBuckets('day', generateDailyLabels, start, end, local); + }, + }, + { + scale: 'hourly', + getBuckets: (start, end, local) => { + const generateHourlyLabels = (ts: number) => { + const formatter = new Intl.DateTimeFormat('en-US', { hour: 'numeric' }); + return () => { + return `${formatter.format(ts)} - ${formatter.format(ts + TIME_IN_MS.hour)}`; + }; + }; + + return produceBuckets('hour', generateHourlyLabels, start, end, local); + }, + }, + { + scale: 'minutes', + getBuckets: (start, end, local) => { + const generateMinuteLabels = (ts: number) => { + const formatter = new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: 'numeric', hour12: false }); + return () => { + return `${formatter.format(ts)}-${formatter.format(ts + TIME_IN_MS.minute)}`; + }; + }; + return produceBuckets('minute', generateMinuteLabels, start, end, local); + }, + }, + { + scale: 'seconds', + getBuckets: (start, end, local) => { + const generateSecondsLabels = (ts: number) => { + const formatter = new Intl.DateTimeFormat('en-US', { second: '2-digit' }); + return () => { + return `${formatter.format(ts)}-${formatter.format(ts + TIME_IN_MS.second)}`; + }; + }; + return produceBuckets('second', generateSecondsLabels, start, end, local); + }, + }, +]; + +export const getTimeBuckets = ( + resultsStartTimes: Array>, + local?: string +): TraceHeatMapTimeBucket => { + const startTimeStamp = min(...resultsStartTimes.map((i) => i.startTimeUnixMs)); + const endTimeStamp = max(...resultsStartTimes.map((i) => trunc(i.startTimeUnixMs + i.durationMs))); + const scale = findScale(startTimeStamp, endTimeStamp); + if (!scale) { + return []; + } + const { getBuckets } = MILLISECONDS_TIME_SCALE.find((mts) => mts.scale === scale) || {}; + if (!getBuckets) { + return []; + } + return getBuckets(startTimeStamp, endTimeStamp, local ?? 'en-US'); +}; diff --git a/traceheatmapchart/src/traceheatmap-tooltip-util.test.ts b/traceheatmapchart/src/traceheatmap-tooltip-util.test.ts new file mode 100644 index 000000000..055ad9ff6 --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-tooltip-util.test.ts @@ -0,0 +1,192 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { TopLevelFormatterParams } from 'echarts/types/dist/shared'; +import { createBucketToolTip, ToolTipChartData } from './traceheatmap-tooltip-util'; + +const mockData = { + params: { + value: [0, 5, 2], + }, + toolTipChartData: { + bucketCountMap: [ + [0, 1, 0, 0, 1, 2, 0, 0, 0, 1, 0], + [1, 0, 0, 1, 2, 2, 0, 1, 0, 3, 0], + [0, 0, 0, 1, 2, 0, 1, 1, 0, 0, 0], + ], + bucketToFlatResultsMap: [ + [[], [15], [], [], [17], [16, 19], [], [], [], [18], []], + [[8], [], [], [14], [6, 11], [5, 10], [], [13], [], [7, 9, 12], []], + [[], [], [], [2], [0, 4], [], [3], [1], [], [], []], + ], + durationBuckets: [ + { + start: 8, + label: '8 ms', + }, + { + start: 16, + label: '16 ms', + }, + { + start: 32, + label: '32 ms', + }, + { + start: 64, + label: '64 ms', + }, + { + start: 128, + label: '128 ms', + }, + { + start: 256, + label: '256 ms', + }, + { + start: 512, + label: '512 ms', + }, + { + start: 1024, + label: '1024 ms', + }, + { + start: 2048, + label: '2048 ms', + }, + { + start: 4096, + label: '4096 ms', + }, + { + start: 8192, + label: '8192 ms', + }, + ], + timeBuckets: [ + { + startTimestamp: 1784540414538.0178, + label: '11:40-11:41', + }, + { + startTimestamp: 1784540474538.0178, + label: '11:41-11:42', + }, + { + startTimestamp: 1784540534538.0178, + label: '11:42-11:43', + }, + ], + flatResults: [ + { + startTimeUnixMs: 1784540566155.6975, + durationMs: 150, + }, + { + // eslint-disable-next-line no-loss-of-precision + startTimeUnixMs: 1784540562271.2812, + durationMs: 2000, + }, + { + startTimeUnixMs: 1784540562188.8926, + durationMs: 81, + }, + { + startTimeUnixMs: 1784540540915.67, + durationMs: 900, + }, + { + startTimeUnixMs: 1784540538764.3755, + durationMs: 150, + }, + { + startTimeUnixMs: 1784540533028.299, + durationMs: 400, + }, + { + startTimeUnixMs: 1784540503971.5842, + durationMs: 150, + }, + { + startTimeUnixMs: 1784540498970.7666, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784540498960.0603, + durationMs: 10, + }, + { + startTimeUnixMs: 1784540493838.0383, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784540492564.4324, + durationMs: 400, + }, + { + startTimeUnixMs: 1784540486921.2476, + durationMs: 150, + }, + { + startTimeUnixMs: 1784540481716.7546, + durationMs: 5001, + }, + { + startTimeUnixMs: 1784540479520.5488, + durationMs: 2001, + }, + { + startTimeUnixMs: 1784540479439.648, + durationMs: 80, + }, + { + startTimeUnixMs: 1784540458808.4639, + durationMs: 31, + }, + { + startTimeUnixMs: 1784540432018.206, + durationMs: 400, + }, + { + startTimeUnixMs: 1784540431867.385, + durationMs: 150, + }, + { + startTimeUnixMs: 1784540414938.3296, + durationMs: 5000, + }, + { + startTimeUnixMs: 1784540414538.0178, + durationMs: 400, + }, + ], + }, +}; + +describe('traceheatmap-tooltip-util', () => { + it('should create a tooltip with all elements', () => { + const { params, toolTipChartData } = mockData; + const toolTip = createBucketToolTip(params as TopLevelFormatterParams, toolTipChartData as ToolTipChartData); + const container = document.createElement('div'); + container.setAttribute('id', 'test-container'); + container.innerHTML = toolTip; + const [column, row] = params.value; + ['time', 'duration', 'traces', 'shared', 'min', 'max'].forEach((element) => { + const id = `bucket-${column}-${row}-tooltip-${element}`; + console.log(id); + expect(container.querySelector(`#${id}`)).not.toBeNull(); + }); + }); +}); diff --git a/traceheatmapchart/src/traceheatmap-tooltip-util.ts b/traceheatmapchart/src/traceheatmap-tooltip-util.ts new file mode 100644 index 000000000..168ec397f --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-tooltip-util.ts @@ -0,0 +1,119 @@ +// Copyright The Perses Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* shortened */ +const min = Math.min; +const max = Math.max; + +import { TopLevelFormatterParams } from 'echarts/types/dist/shared'; +import { HeatmapChartData, FlatQueryResults } from './traceheatmap-chart-data-util'; + +export type ToolTipChartData = Pick< + HeatmapChartData, + 'durationBuckets' | 'timeBuckets' | 'bucketCountMap' | 'bucketToFlatResultsMap' +> & { flatResults: FlatQueryResults }; + +export const createBucketToolTip = (params: TopLevelFormatterParams, toolTipChartData: ToolTipChartData): string => { + if (Array.isArray(params)) { + return ''; + } + + const { bucketCountMap, durationBuckets, timeBuckets, bucketToFlatResultsMap, flatResults } = toolTipChartData; + const [column, row, count] = params.value as [number, number, number]; + const bucketDetails: Array<{ label: string; value: string | undefined; elementId: string }> = []; + + const generateId = (column: number, row: number, element: string): string => + `bucket-${column}-${row}-tooltip-${element}`; + + bucketDetails.push({ + label: 'Time', + value: timeBuckets[column]?.label, + elementId: generateId(column, row, 'time'), + }); + + bucketDetails.push({ + label: 'Duration', + elementId: generateId(column, row, 'duration'), + value: ((): string => { + const begin = durationBuckets[row]; + const end = durationBuckets[row + 1]; + if (end === undefined) { + return `+${begin?.start} ms`; + } + return `${begin?.start}ms-${end.start}ms`; + })(), + }); + + bucketDetails.push({ + label: 'Traces', + value: Number(count) + '', + elementId: generateId(column, row, 'traces'), + }); + + bucketDetails.push({ + label: 'Share of traces', + elementId: generateId(column, row, 'shared'), + value: ((): string | undefined => { + const total = (bucketCountMap[column] || []).reduce((prev, current) => { + return typeof current === 'number' ? prev + current : prev; + }, 0); + const current = bucketCountMap[column]?.[row]; + return total === undefined || current === undefined ? undefined : `${((current / total) * 100).toFixed(2)}%`; + })(), + }); + + bucketDetails.push({ + label: 'Min', + elementId: generateId(column, row, 'min'), + value: ((): string | undefined => { + const indexes = bucketToFlatResultsMap[column]?.[row]; + if (!indexes) return undefined; + return ( + min(...indexes.filter((idx) => flatResults[idx] !== undefined).map((idx) => flatResults[idx]!.durationMs)) + '' + ); + })(), + }); + + bucketDetails.push({ + label: 'Max', + elementId: generateId(column, row, 'max'), + value: ((): string | undefined => { + const indexes = bucketToFlatResultsMap[column]?.[row]; + if (!indexes) return undefined; + return ( + max(...indexes.filter((idx) => flatResults[idx] !== undefined).map((idx) => flatResults[idx]!.durationMs)) + '' + ); + })(), + }); + + return `
+ ${((): string => { + return bucketDetails + .filter((i) => i.value !== undefined) + .map( + (i) => `
+ ${i.label}: ${i.value} +
` + ) + .join(''); + })()} +
`; +}; diff --git a/traceheatmapchart/tsconfig.build.json b/traceheatmapchart/tsconfig.build.json new file mode 100644 index 000000000..fc0aafe27 --- /dev/null +++ b/traceheatmapchart/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["**/*.stories.*", "**/*.test.*", "**/*.map"], + "compilerOptions": { + "emitDeclarationOnly": true, + "declaration": true, + "preserveWatchOutput": true + } +} diff --git a/traceheatmapchart/tsconfig.json b/traceheatmapchart/tsconfig.json new file mode 100644 index 000000000..40e3c4dfe --- /dev/null +++ b/traceheatmapchart/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "outDir": "./dist/lib", + "rootDir": "./src", + "target": "es2022", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "esnext", + "jsx": "react-jsx", + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noUncheckedIndexedAccess": true, + "declaration": true, + "declarationMap": true, + "pretty": true + }, + "include": ["src"] +} \ No newline at end of file