diff --git a/package-lock.json b/package-lock.json index 37f3f73f8..60cfa3290 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "tracetable", "tracingganttchart", "victorialogs", + "traceheatmapchart", "e2e" ], "devDependencies": { @@ -3825,6 +3826,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", @@ -17403,6 +17408,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-beta.3", diff --git a/package.json b/package.json index d4ce6c680..fca036cb2 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "tracetable", "tracingganttchart", "victorialogs", + "traceheatmapchart", "e2e" ], "peerDependencies": { 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..20cacfb0d --- /dev/null +++ b/traceheatmapchart/.eslintrc.js @@ -0,0 +1,65 @@ +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..6075e02c7 --- /dev/null +++ b/traceheatmapchart/cue.mod/module.cue @@ -0,0 +1,13 @@ +module: "github.com/perses/traceheatmapchart@v0" +language: { + version: "v0.12.0" +} +source: { + kind: "git" +} +deps: { + "github.com/perses/perses/cue@v0": { + v: "v0.51.0-preview" + 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..72799d2db --- /dev/null +++ b/traceheatmapchart/jest.config.ts @@ -0,0 +1,13 @@ +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/percli.exe b/traceheatmapchart/percli.exe new file mode 100644 index 000000000..ccbe38d30 Binary files /dev/null and b/traceheatmapchart/percli.exe differ 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..54140a7e8 --- /dev/null +++ b/traceheatmapchart/schemas/traceheatmapchart.cue @@ -0,0 +1,27 @@ +package model + +import ( + "github.com/perses/perses/cue/common" +) + +#legendValue: common.#calculation + +#legend: { + position: "bottom" | "right" + mode?: "list" | "table" + size?: "small" | "medium" + values?: [...#legendValue] +} + +#querySettings: [...{ + queryIndex: int & >=0 + colorMode: "fixed" | "fixed-single" + colorValue: =~"^#(?:[0-9a-fA-F]{3}){1,2}$" // hexadecimal color code +}] + +kind: "Traceheatmapchart" +spec: close({ + legend?: #legend + thresholds?: common.#thresholds + querySettings?: #querySettings +}) diff --git a/traceheatmapchart/schemas/traceheatmapchart.json b/traceheatmapchart/schemas/traceheatmapchart.json new file mode 100644 index 000000000..2608bdafd --- /dev/null +++ b/traceheatmapchart/schemas/traceheatmapchart.json @@ -0,0 +1,20 @@ +{ + "kind": "Traceheatmapchart", + "spec": { + "legend": { + "position": "bottom" + }, + "thresholds": { + "steps": [ + { + "value": 0.6, + "name": "Alert: Warning condition example" + }, + { + "value": 0.8, + "name": "Alert: Critical condition example" + } + ] + } + } +} diff --git a/traceheatmapchart/src/TraceheatmapChart.ts b/traceheatmapchart/src/TraceheatmapChart.ts new file mode 100644 index 000000000..c8ea8da67 --- /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 { TraceheatmapGeneralSettings } from './TraceheatmapGeneralSettings'; + +import { TraceheatmapChartPanel, TraceheatmapChartProps } from './TraceheatmapChartPanel'; + +export const Traceheatmapchart: PanelPlugin = { + PanelComponent: TraceheatmapChartPanel, + supportedQueryTypes: ['TraceQuery'], + panelOptionsEditorComponents: [{ label: 'General Settings', content: TraceheatmapGeneralSettings }], + createInitialOptions: createInitialTraceheatmapChartOptions, +}; diff --git a/traceheatmapchart/src/TraceheatmapChartBase.tsx b/traceheatmapchart/src/TraceheatmapChartBase.tsx new file mode 100644 index 000000000..a11df8eb6 --- /dev/null +++ b/traceheatmapchart/src/TraceheatmapChartBase.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 { ReactElement } from 'react'; + +export const TraceheatmapChartBase = (): ReactElement => { + return <>; +}; diff --git a/traceheatmapchart/src/TraceheatmapChartPanel.tsx b/traceheatmapchart/src/TraceheatmapChartPanel.tsx new file mode 100644 index 000000000..4f6dae89c --- /dev/null +++ b/traceheatmapchart/src/TraceheatmapChartPanel.tsx @@ -0,0 +1,176 @@ +// 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, TraceSearchResult } from '@perses-dev/spec'; +import { EChart, useChartsContext } from '@perses-dev/components'; +import { ECharts as EChartsInstance } from 'echarts/core'; +import { EChartsOption } from 'echarts'; +import { TraceheatmapOptions } from './traceheatmap-chart-model'; +import { getTimeBuckets } from './traceheatmap-time-bucket-util'; +import { getDurationBuckets } from './traceheatmap-duration-bucket-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: Array = []; + queryResults.forEach((qr, idx) => { + (qr.data.searchResult || []).forEach((sr) => { + allFlatResults.push({ ...sr, idx }); + }); + }); + return allFlatResults; + }, [queryResults]); + + const [cellWeightMap, metaDataMap, timeBuckets, durationBuckets] = useMemo(() => { + /* generate buckets */ + const timeBuckets = getTimeBuckets( + allFlatResults.map((r) => ({ durationMs: r.durationMs, startTimeUnixMs: r.startTimeUnixMs })) + ); + const durationBuckets = getDurationBuckets( + allFlatResults.map((qst) => qst.durationMs), + base + ); + + /* init plotter and metadata arrays */ + const cellWeightMap: number[][] = []; + const metaDataArray: number[][][] = []; + + timeBuckets.forEach(() => { + const valueRow: number[] = []; + const metaDataMap: number[][] = []; + durationBuckets.forEach(() => { + valueRow.push(0); + metaDataMap.push([]); + }); + cellWeightMap.push(valueRow); + metaDataArray.push(metaDataMap); + }); + + /* row, col, value */ + + for (let i = 0; i < allFlatResults.length; i += 1) { + const { startTimeUnixMs, durationMs, idx } = allFlatResults[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; + } + } + + cellWeightMap[timeBucketIndex]![durationBucketIndex]! += 1; + metaDataArray[timeBucketIndex]![durationBucketIndex]!.push(idx); + } + + return [cellWeightMap, metaDataArray, timeBuckets, durationBuckets]; + }, [allFlatResults, base]); + + const chartData: number[][] = []; + cellWeightMap.forEach((r, rIdx) => { + r.forEach((c, cIdx) => { + chartData.push([cIdx, rIdx, c]); + }); + }); + + const option: EChartsOption = { + tooltip: { + position: 'top', + }, + grid: { + height: '50%', + top: '10%', + }, + 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: 0, + max: 10, + calculable: true, + orient: 'horizontal', + left: 'center', + bottom: '15%', + }, + series: [ + { + name: 'Punch Card', + type: 'heatmap', + data: chartData, + label: { + show: true, + }, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowColor: 'rgba(0, 0, 0, 0.5)', + }, + }, + }, + ], + }; + + return ( + <> + + + ); +}; diff --git a/traceheatmapchart/src/TraceheatmapGeneralSettings.tsx b/traceheatmapchart/src/TraceheatmapGeneralSettings.tsx new file mode 100644 index 000000000..801ddd19f --- /dev/null +++ b/traceheatmapchart/src/TraceheatmapGeneralSettings.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 { ReactElement } from 'react'; + +export const TraceheatmapGeneralSettings = (): ReactElement => { + return <>; +}; diff --git a/traceheatmapchart/src/bootstrap.tsx b/traceheatmapchart/src/bootstrap.tsx new file mode 100644 index 000000000..515087f4d --- /dev/null +++ b/traceheatmapchart/src/bootstrap.tsx @@ -0,0 +1,5 @@ +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..b0ac762b0 --- /dev/null +++ b/traceheatmapchart/src/env.d.ts @@ -0,0 +1 @@ +/// diff --git a/traceheatmapchart/src/getPluginModule.ts b/traceheatmapchart/src/getPluginModule.ts new file mode 100644 index 000000000..4969963ae --- /dev/null +++ b/traceheatmapchart/src/getPluginModule.ts @@ -0,0 +1,17 @@ +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..50599f7ad --- /dev/null +++ b/traceheatmapchart/src/index-federation.ts @@ -0,0 +1 @@ +import("./bootstrap"); diff --git a/traceheatmapchart/src/index.ts b/traceheatmapchart/src/index.ts new file mode 100644 index 000000000..aee80e228 --- /dev/null +++ b/traceheatmapchart/src/index.ts @@ -0,0 +1 @@ +export { getPluginModule } from './getPluginModule'; diff --git a/traceheatmapchart/src/setup-tests.ts b/traceheatmapchart/src/setup-tests.ts new file mode 100644 index 000000000..ef31bd722 --- /dev/null +++ b/traceheatmapchart/src/setup-tests.ts @@ -0,0 +1,4 @@ +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/traceheatmap-chart-model.ts b/traceheatmapchart/src/traceheatmap-chart-model.ts new file mode 100644 index 000000000..cd7faf34a --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-chart-model.ts @@ -0,0 +1,35 @@ +// 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 TraceheatmapBucket { + label: string; + min: number; + max: number; + count: number; + traces: Array<{ id: string; durationMs: number }>; +} + +export interface BucketSettings { + base: ExponentialBase; +} + +export type TraceheatmapGrowthFactor = 1 | 1.2 | 2; +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..0f90f720e --- /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 ExponentialBuckets = 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): ExponentialBuckets => { + const cleanDuration = durations.filter((d) => d > 0); + const minDuration = min(...cleanDuration); + const maxDuration = max(...cleanDuration); + const buckets: ExponentialBuckets = []; + + 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..058af6070 --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-time-bucket-util.test.ts @@ -0,0 +1,165 @@ +// 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: 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: '1-2,2-3,3-4,4-5', + }, + { + start: 1784159880000, + end: 1784160300000, + desc: 'should include minutes', + testPurpose: 'stretched minutes scaled', + exp: '58-59,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..0a2b3c08f --- /dev/null +++ b/traceheatmapchart/src/traceheatmap-time-bucket-util.ts @@ -0,0 +1,208 @@ +// 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'; + +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'; + } + 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', { minute: '2-digit' }); + return () => { + return `${formatter.format(ts)}-${formatter.format(ts + TIME_IN_MS.minute)}`; + }; + }; + return produceBuckets('minute', generateMinuteLabels, 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) { + throw new Error( + `Range ${new Date(startTimeStamp).toISOString()} to ${new Date(endTimeStamp).toISOString()} does not produce any valid scale for the trace heatmap chart` + ); + } + const { getBuckets } = MILLISECONDS_TIME_SCALE.find((mts) => mts.scale === scale) || {}; + if (!getBuckets) { + return []; + } + return getBuckets(startTimeStamp, endTimeStamp, local ?? 'en-US'); +}; 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