├── .dockerignore ├── .editorconfig ├── .eslintrc.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── ci.yml │ ├── codeql-analysis.yml │ └── release.yml ├── .gitignore ├── .prettierrc.js ├── .readthedocs.yaml ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── Magefile.go ├── Makefile ├── NOTICE ├── README.md ├── babel.config.js ├── ci └── scripts │ └── github-release.sh ├── config └── jest-setup.ts ├── cypress.config.ts ├── cypress ├── e2e │ ├── checklist.cy.ts │ ├── datasources │ │ ├── valkey.cy.ts │ │ └── vector.cy.ts │ ├── login.cy.ts │ └── setup.cy.ts ├── plugins │ └── index.ts ├── support │ ├── commands.ts │ └── e2e.ts └── tsconfig.json ├── docs ├── .gitignore ├── CHANGELOG.md ├── Makefile ├── _static │ └── custom.css ├── architecture.rst ├── conf.py ├── datasources │ ├── authentication.rst │ ├── bpftrace.rst │ ├── index.rst │ ├── valkey.rst │ └── vector.rst ├── guides │ ├── containers.rst │ └── multiple-vector-hosts.rst ├── img │ └── architecture.png ├── index.rst ├── installation.rst ├── pcp-valkey.md ├── quickstart.rst ├── refs.rst ├── requirements.txt ├── screenshots.rst └── troubleshooting.rst ├── go.mod ├── go.sum ├── jest.config.js ├── jsonnetfile.json ├── jsonnetfile.lock.json ├── package.json ├── pkg ├── datasources │ └── valkey │ │ ├── api │ │ └── pmseries │ │ │ ├── models.go │ │ │ └── pmseries.go │ │ ├── data_processor.go │ │ ├── datasource.go │ │ ├── datasource_test.go │ │ ├── field_transformations.go │ │ ├── field_transformations_test.go │ │ ├── models.go │ │ ├── query.go │ │ ├── resource │ │ ├── metric_find_value.go │ │ ├── models.go │ │ └── resource.go │ │ ├── series │ │ ├── models.go │ │ ├── series.go │ │ └── series_test.go │ │ ├── test │ │ └── fixtures │ │ │ └── pmseriesf │ │ │ ├── fixtures.go │ │ │ └── generators.go │ │ └── utils.go └── main.go ├── src ├── common │ ├── services │ │ ├── pmapi │ │ │ ├── PmApiService.ts │ │ │ └── types.ts │ │ ├── pmsearch │ │ │ ├── PmSearchApiService.ts │ │ │ └── types.ts │ │ └── pmseries │ │ │ ├── PmSeriesApiService.ts │ │ │ └── types.ts │ ├── types │ │ ├── errors.ts │ │ ├── pcp.ts │ │ └── utils.ts │ └── utils.ts ├── components │ ├── app │ │ └── App.tsx │ ├── appconfig │ │ ├── config.tsx │ │ └── types.ts │ ├── monaco │ │ ├── MonacoEditorLazy.tsx │ │ ├── MonacoEditorWrapper.tsx │ │ └── index.ts │ └── search │ │ ├── App.test.tsx │ │ ├── App.tsx │ │ ├── Search.tsx │ │ ├── components │ │ ├── BookmarkList │ │ │ ├── BookmarkList.test.tsx │ │ │ ├── BookmarkList.tsx │ │ │ └── styles.ts │ │ ├── Card │ │ │ ├── Card.test.tsx │ │ │ ├── Card.tsx │ │ │ └── styles.ts │ │ ├── Loader │ │ │ ├── Loader.test.tsx │ │ │ ├── Loader.tsx │ │ │ └── styles.ts │ │ ├── SearchHistoryList │ │ │ ├── SearchHistoryList.test.tsx │ │ │ ├── SearchHistoryList.tsx │ │ │ └── styles.ts │ │ ├── SearchResult │ │ │ ├── SearchResult.test.tsx │ │ │ ├── SearchResult.tsx │ │ │ └── styles.ts │ │ └── withServices │ │ │ ├── withServices.test.tsx │ │ │ └── withServices.tsx │ │ ├── config │ │ └── config.ts │ │ ├── contexts │ │ └── services.ts │ │ ├── mocks │ │ ├── endpoints.ts │ │ └── responses.ts │ │ ├── models │ │ ├── endpoints │ │ │ └── pmapi.ts │ │ ├── entities │ │ │ ├── indom.ts │ │ │ └── metric.ts │ │ └── errors │ │ │ └── errors.ts │ │ ├── pages │ │ ├── Detail │ │ │ ├── DetailPage.test.tsx │ │ │ ├── DetailPage.tsx │ │ │ ├── InstanceDomain │ │ │ │ ├── InstanceDomain.test.tsx │ │ │ │ ├── InstanceDomain.tsx │ │ │ │ └── Instances │ │ │ │ │ ├── Instances.test.tsx │ │ │ │ │ └── Instances.tsx │ │ │ ├── Metric │ │ │ │ ├── Labels │ │ │ │ │ ├── Labels.test.tsx │ │ │ │ │ └── Labels.tsx │ │ │ │ ├── Meta │ │ │ │ │ ├── Meta.test.tsx │ │ │ │ │ └── Meta.tsx │ │ │ │ ├── Metric.test.tsx │ │ │ │ ├── Metric.tsx │ │ │ │ └── Series │ │ │ │ │ ├── Series.test.tsx │ │ │ │ │ └── Series.tsx │ │ │ └── styles.ts │ │ ├── Index │ │ │ ├── IndexPage.test.tsx │ │ │ ├── IndexPage.tsx │ │ │ └── styles.ts │ │ └── Search │ │ │ ├── SearchPage.test.tsx │ │ │ ├── SearchPage.tsx │ │ │ └── styles.ts │ │ ├── partials │ │ ├── Actions │ │ │ ├── Actions.test.tsx │ │ │ ├── Actions.tsx │ │ │ └── styles.ts │ │ ├── Aside │ │ │ ├── Aside.test.tsx │ │ │ ├── Aside.tsx │ │ │ └── styles.ts │ │ └── SearchForm │ │ │ ├── SearchForm.test.tsx │ │ │ ├── SearchForm.tsx │ │ │ └── styles.ts │ │ ├── services │ │ ├── EntityDetailService.ts │ │ └── services.ts │ │ ├── store │ │ ├── reducer.ts │ │ ├── slices │ │ │ └── search │ │ │ │ ├── reducer.ts │ │ │ │ ├── shared │ │ │ │ ├── actionCreators.ts │ │ │ │ └── state.ts │ │ │ │ └── slices │ │ │ │ ├── bookmarks │ │ │ │ ├── actionCreators.ts │ │ │ │ ├── actions.ts │ │ │ │ ├── reducer.ts │ │ │ │ ├── state.ts │ │ │ │ └── types.ts │ │ │ │ ├── entity │ │ │ │ ├── actionCreators.ts │ │ │ │ ├── actions.ts │ │ │ │ ├── reducer.ts │ │ │ │ ├── state.ts │ │ │ │ └── types.ts │ │ │ │ ├── history │ │ │ │ ├── actionCreators.ts │ │ │ │ ├── actions.ts │ │ │ │ ├── reducer.ts │ │ │ │ ├── state.ts │ │ │ │ └── types.ts │ │ │ │ ├── query │ │ │ │ ├── actions.ts │ │ │ │ ├── reducer.ts │ │ │ │ ├── state.ts │ │ │ │ └── types.ts │ │ │ │ ├── result │ │ │ │ ├── actions.ts │ │ │ │ ├── reducer.ts │ │ │ │ ├── state.ts │ │ │ │ └── types.ts │ │ │ │ └── view │ │ │ │ ├── actionCreators.ts │ │ │ │ ├── actions.ts │ │ │ │ ├── reducer.ts │ │ │ │ ├── state.ts │ │ │ │ └── types.ts │ │ └── store.ts │ │ ├── styles.ts │ │ └── utils │ │ ├── SearchEntityUtil.ts │ │ └── utils.ts ├── dashboards │ ├── prometheus │ │ └── pcp-prometheus-host-overview.jsonnet │ ├── valkey │ │ └── preview │ │ │ ├── pcp-valkey-metric-preview-graph.jsonnet │ │ │ └── pcp-valkey-metric-preview-table.jsonnet │ └── vector │ │ └── checklist │ │ ├── _breadcrumbspanel.libsonnet │ │ ├── _troubleshootingpanel.libsonnet │ │ ├── checklist.libsonnet │ │ ├── pcp-vector-checklist-cpu-sys.jsonnet │ │ ├── pcp-vector-checklist-cpu-user.jsonnet │ │ ├── pcp-vector-checklist-cpu.jsonnet │ │ ├── pcp-vector-checklist-memory-swap.jsonnet │ │ ├── pcp-vector-checklist-memory.jsonnet │ │ ├── pcp-vector-checklist-network-rx.jsonnet │ │ ├── pcp-vector-checklist-network-tx.jsonnet │ │ ├── pcp-vector-checklist-network.jsonnet │ │ ├── pcp-vector-checklist-storage.jsonnet │ │ └── pcp-vector-checklist.jsonnet ├── datasources │ ├── bpftrace │ │ ├── README.md │ │ ├── components │ │ │ ├── BPFtraceQueryEditor.tsx │ │ │ └── language │ │ │ │ ├── BPFtraceBuiltins.json │ │ │ │ ├── BPFtraceCompletionItemProvider.ts │ │ │ │ └── BPFtraceLanguage.ts │ │ ├── config.ts │ │ ├── configuration │ │ │ └── BPFtraceConfigEditor.tsx │ │ ├── dashboards │ │ │ ├── pcp-bpftrace-flame-graphs.jsonnet │ │ │ ├── pcp-bpftrace-system-analysis.jsonnet │ │ │ └── tools │ │ │ │ ├── biolatency.bt │ │ │ │ ├── cpuwalk.bt │ │ │ │ ├── kstacks.bt │ │ │ │ ├── runqlat.bt │ │ │ │ ├── runqlen.bt │ │ │ │ ├── syscall_count.bt │ │ │ │ ├── tcpaccept.bt │ │ │ │ ├── tcpconnect.bt │ │ │ │ ├── tcpdrop.bt │ │ │ │ ├── tcplife.bt │ │ │ │ ├── tcpretrans.bt │ │ │ │ ├── ustacks.bt │ │ │ │ └── vfscount.bt │ │ ├── datasource.test.ts │ │ ├── datasource.ts │ │ ├── img │ │ │ └── eBPF.png │ │ ├── module.ts │ │ ├── plugin.json │ │ ├── script.ts │ │ ├── script_manager.ts │ │ └── types.ts │ ├── lib │ │ ├── language.ts │ │ ├── pmapi │ │ │ ├── data_processor.test.ts │ │ │ ├── data_processor.ts │ │ │ ├── datasource_base.ts │ │ │ ├── field_transformations.test.ts │ │ │ ├── field_transformations.ts │ │ │ ├── poller │ │ │ │ ├── poller.ts │ │ │ │ └── types.ts │ │ │ └── types.ts │ │ ├── specs │ │ │ ├── fixtures │ │ │ │ ├── datasource.ts │ │ │ │ ├── grafana.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pcp.ts │ │ │ │ ├── pmapi.ts │ │ │ │ ├── pmseries.ts │ │ │ │ └── poller.ts │ │ │ └── mocks │ │ │ │ └── backend_srv.ts │ │ └── types.ts │ ├── valkey │ │ ├── README.md │ │ ├── components │ │ │ ├── ValkeyQueryEditor.tsx │ │ │ └── language │ │ │ │ ├── PmseriesBuiltins.json │ │ │ │ ├── PmseriesCompletionItemProvider.ts │ │ │ │ └── PmseriesLanguage.ts │ │ ├── configuration │ │ │ └── ValkeyConfigEditor.tsx │ │ ├── dashboards │ │ │ ├── pcp-valkey-host-overview.jsonnet │ │ │ └── pcp-valkey-mssql-server.jsonnet │ │ ├── datasource.ts │ │ ├── img │ │ │ ├── valkey.png │ │ │ └── valkey.svg │ │ ├── module.ts │ │ ├── plugin.json │ │ └── types.ts │ └── vector │ │ ├── DEV_NOTES.md │ │ ├── README.md │ │ ├── __snapshots__ │ │ └── datasource.test.ts.snap │ │ ├── components │ │ ├── VectorQueryEditor.tsx │ │ └── language │ │ │ ├── PmapiBuiltins.json │ │ │ ├── PmapiCompletionItemProvider.ts │ │ │ └── PmapiLanguage.ts │ │ ├── config.ts │ │ ├── configuration │ │ └── VectorConfigEditor.tsx │ │ ├── dashboards │ │ ├── pcp-vector-bcc-overview.jsonnet │ │ ├── pcp-vector-container-overview-cgroups1.jsonnet │ │ ├── pcp-vector-container-overview-cgroups2.jsonnet │ │ ├── pcp-vector-host-overview.jsonnet │ │ ├── pcp-vector-mssql-server.jsonnet │ │ ├── pcp-vector-top-consumers.jsonnet │ │ └── pcp-vector-uwsgi-overview.jsonnet │ │ ├── datasource.test.ts │ │ ├── datasource.ts │ │ ├── img │ │ ├── vector.png │ │ └── vector.svg │ │ ├── module.ts │ │ ├── plugin.json │ │ └── types.ts ├── img │ ├── pcp-logo.svg │ └── screenshots │ │ ├── bpftrace-cpu.png │ │ ├── bpftrace-disk.png │ │ ├── bpftrace-flame-graph.png │ │ ├── bpftrace-function-autocompletion.png │ │ ├── bpftrace-probe-autocompletion.png │ │ ├── bpftrace-tcp.png │ │ ├── bpftrace-variable-autocompletion.png │ │ ├── search.png │ │ ├── vector-bcc-overview1.png │ │ ├── vector-bcc-overview2.png │ │ ├── vector-checklist.png │ │ ├── vector-containers.png │ │ ├── vector-metric-autocompletion.png │ │ └── vector-overview.png ├── module.ts ├── panels │ ├── breadcrumbs │ │ ├── BreadcrumbsPanel.tsx │ │ ├── img │ │ │ └── pcp-logo.svg │ │ ├── module.tsx │ │ ├── plugin.json │ │ ├── styles.ts │ │ └── types.ts │ ├── flamegraph │ │ ├── FlameGraphChart.tsx │ │ ├── FlameGraphPanel.test.skip.tsx │ │ ├── FlameGraphPanel.tsx │ │ ├── css │ │ │ └── flamegraph.css │ │ ├── img │ │ │ └── flamegraph.svg │ │ ├── model.test.ts │ │ ├── model.ts │ │ ├── module.tsx │ │ ├── plugin.json │ │ └── types.ts │ └── troubleshooting │ │ ├── TroubleshootingPane.tsx │ │ ├── TroubleshootingPanel.tsx │ │ ├── img │ │ └── pcp-logo.svg │ │ ├── module.tsx │ │ ├── plugin.json │ │ ├── styles.ts │ │ └── types.ts └── plugin.json ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | e2e 2 | dist 3 | node_modules 4 | vendor_jsonnet 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 4 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | max_line_length = 120 11 | 12 | [*.{js,ts,tsx,scss}] 13 | quote_type = single 14 | 15 | [*.{yml,jsonnet,libsonnet}] 16 | indent_size = 2 17 | 18 | [Makefile] 19 | indent_style = tab 20 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@grafana/eslint-config" 4 | ], 5 | "parserOptions": { 6 | "project": "./tsconfig.json" 7 | }, 8 | "rules": { 9 | "@typescript-eslint/no-floating-promises": "error", 10 | "no-restricted-globals": [ 11 | "error", 12 | "name", 13 | "close", 14 | "history", 15 | "length", 16 | "open", 17 | "parent", 18 | "scroll", 19 | "stop", 20 | "event" 21 | ], 22 | "no-console": [ 23 | "error", 24 | {} 25 | ] 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://hcb.hackclub.com/donations/start/pcp 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Data source:** (please fill in) 11 | 12 | **Describe the bug** 13 | A clear and concise description of what the bug is. 14 | 15 | **To Reproduce** 16 | Steps to reproduce the behavior: 17 | 1. Go to '...' 18 | 2. Click on '....' 19 | 3. Scroll down to '....' 20 | 4. See error 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Versions (please complete the following information):** 29 | - Performance Co-Pilot: 30 | - Grafana: 31 | - grafana-pcp: 32 | - Valkey (if applicable): 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Data source:** (please fill in) 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Additional context** 22 | Add any other context or screenshots about the feature request here. 23 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [ main ] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [ main ] 14 | schedule: 15 | - cron: '35 17 * * 5' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | permissions: 22 | actions: read 23 | contents: read 24 | security-events: write 25 | 26 | strategy: 27 | fail-fast: false 28 | matrix: 29 | language: [ 'go', 'typescript' ] 30 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 31 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 32 | 33 | steps: 34 | - name: Checkout repository 35 | uses: actions/checkout@v3 36 | 37 | - name: Setup Node.js 17 38 | uses: actions/setup-node@v3 39 | with: 40 | node-version: 17 41 | 42 | # Initializes the CodeQL tools for scanning. 43 | - name: Initialize CodeQL 44 | uses: github/codeql-action/init@v2 45 | with: 46 | languages: ${{ matrix.language }} 47 | # If you wish to specify custom queries, you can do so here or in a config file. 48 | # By default, queries listed here will override any specified in a config file. 49 | # Prefix the list here with "+" to use these queries and those in the config file. 50 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 51 | 52 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 53 | # If this step fails, then you should remove it and run the build manually (see below) 54 | - name: Autobuild 55 | uses: github/codeql-action/autobuild@v2 56 | 57 | # ℹ️ Command-line programs to run using the OS shell. 58 | # 📚 https://git.io/JvXDl 59 | 60 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 61 | # and modify them (or add more) to build your code if your project 62 | # uses a compiled language 63 | 64 | #- run: | 65 | # make bootstrap 66 | # make release 67 | 68 | - name: Perform CodeQL Analysis 69 | uses: github/codeql-action/analyze@v2 70 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: 5 | - 'v*' 6 | 7 | jobs: 8 | release: 9 | name: Publish Release 10 | runs-on: ubuntu-22.04 11 | steps: 12 | - name: Checkout sources 13 | uses: actions/checkout@v3 14 | 15 | - name: Setup Node.js 17 16 | uses: actions/setup-node@v3 17 | with: 18 | node-version: 17 19 | 20 | - name: Install jsonnet, jsonnet-bundler and grafana/plugin-validator 21 | run: | 22 | go install github.com/google/go-jsonnet/cmd/jsonnet@latest 23 | go install github.com/jsonnet-bundler/jsonnet-bundler/cmd/jb@latest 24 | go install github.com/grafana/plugin-validator/pkg/cmd/plugincheck2@latest 25 | echo "$HOME/go/bin" >> $GITHUB_PATH 26 | 27 | - name: Install dependencies 28 | run: make deps 29 | 30 | - name: Test & Build plugin 31 | run: | 32 | make test-backend 33 | make build # also runs frontend tests 34 | 35 | - name: Check if package version and git tag matches 36 | run: | 37 | if [ "v$(jq -r '.info.version' dist/plugin.json)" != "${GITHUB_REF#refs/*/}" ]; then 38 | echo "Plugin version doesn't match git tag" 39 | exit 1 40 | fi 41 | 42 | - name: Sign plugin 43 | run: make sign 44 | env: 45 | GRAFANA_API_KEY: ${{ secrets.GRAFANA_API_KEY }} 46 | 47 | - name: Create plugin zip file 48 | run: make zip 49 | 50 | - name: Run plugincheck 51 | run: make plugincheck 52 | 53 | - name: Create GitHub release 54 | run: ci/scripts/github-release.sh 55 | env: 56 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | # Golang vendor dir 3 | vendor/ 4 | vendor_jsonnet/ 5 | 6 | dist/ 7 | build/ 8 | 9 | coverage/ 10 | coverage.out 11 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('./node_modules/@grafana/toolkit/src/config/prettier.plugin.config.json'), 3 | // If this is not here, prettier takes default 'always', which doesnt seem to be the rule that grafana-toolkit is linting by 4 | arrowParens: 'avoid', 5 | importOrder: ["^@grafana/(.*)$", "^[./]"], 6 | }; 7 | -------------------------------------------------------------------------------- /.readthedocs.yaml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-22.04 11 | tools: 12 | python: "3.11" 13 | 14 | # Build documentation in the docs/ directory with Sphinx 15 | sphinx: 16 | configuration: docs/conf.py 17 | 18 | # We recommend specifying your dependencies to enable reproducible builds: 19 | # https://docs.readthedocs.io/en/stable/guides/reproducible-builds.html 20 | python: 21 | install: 22 | - requirements: docs/requirements.txt 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.preferences.importModuleSpecifier": "relative" 3 | } -------------------------------------------------------------------------------- /Magefile.go: -------------------------------------------------------------------------------- 1 | //+build mage 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | // mage:import 8 | build "github.com/grafana/grafana-plugin-sdk-go/build" 9 | ) 10 | 11 | // Hello prints a message (shows that you can define custom Mage targets). 12 | func Hello() { 13 | fmt.Println("hello plugin developer!") 14 | } 15 | 16 | // Default configures the default target. 17 | var Default = build.BuildAll 18 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 simPod & andig 2 | Copyright (c) 2019 Netflix 3 | Copyright (c) 2019-2021 Grafana Labs 4 | Copyright (c) 2019-2021 Red Hat 5 | 6 | Thanks to Jason Koch for the initial pcp-live data source implementation and the host overview dashboard. 7 | The Linux BPF Pony (used for the bpftrace data source) was created by Deirdré Straughan using General Zoi's pony creator. 8 | The flamegraph panel uses d3-flame-graph, Copyright (c) 2018 Martin Spier. 9 | The bpftrace documentation follows the [Reference Guide](https://github.com/iovisor/bpftrace/blob/master/docs/reference_guide.md), written by bpftrace community contributors. 10 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "plugins": [ 3 | [ "react-remove-properties", { "properties": ["data-test"] } ], 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /ci/scripts/github-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -eu 2 | # 3 | # Release process adapted from 4 | # https://github.com/grafana/kentik-app/blob/master/.circleci/config.yml 5 | # https://github.com/marcusolsson/grafana-jsonapi-datasource/blob/master/.github/workflows/main.yml 6 | # 7 | 8 | GRAFANA_PLUGIN_ID="$(jq -r '.id' dist/plugin.json)" 9 | GRAFANA_PLUGIN_VERSION="$(jq -r '.info.version' dist/plugin.json)" 10 | GRAFANA_PLUGIN_ARTIFACT="${GRAFANA_PLUGIN_ID}-${GRAFANA_PLUGIN_VERSION}.zip" 11 | GRAFANA_PLUGIN_ARTIFACT_CHECKSUM="${GRAFANA_PLUGIN_ARTIFACT}.md5" 12 | 13 | RELEASE_NOTES=$(awk '/^## / {s++} s == 1 {print}' CHANGELOG.md) 14 | PRERELEASE_ARG="" 15 | if [[ "${GRAFANA_PLUGIN_VERSION}" == *beta* ]]; then 16 | PRERELEASE_ARG="-p" 17 | fi 18 | 19 | gh release create \ 20 | "v${GRAFANA_PLUGIN_VERSION}" \ 21 | -t "grafana-pcp v${GRAFANA_PLUGIN_VERSION}" \ 22 | -n "${RELEASE_NOTES}" \ 23 | "build/${GRAFANA_PLUGIN_ARTIFACT}" \ 24 | "build/${GRAFANA_PLUGIN_ARTIFACT_CHECKSUM}" \ 25 | ${PRERELEASE_ARG} 26 | -------------------------------------------------------------------------------- /config/jest-setup.ts: -------------------------------------------------------------------------------- 1 | import Enzyme from 'enzyme'; 2 | import EnzymeAdapter from 'enzyme-adapter-react-16'; 3 | 4 | // Setup enzyme's react adapter 5 | Enzyme.configure({ adapter: new EnzymeAdapter() }); 6 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress' 2 | 3 | export default defineConfig({ 4 | e2e: { 5 | setupNodeEvents(on, config) { 6 | return require('./cypress/plugins/index.ts').default(on, config) 7 | }, 8 | }, 9 | }) 10 | -------------------------------------------------------------------------------- /cypress/e2e/checklist.cy.ts: -------------------------------------------------------------------------------- 1 | describe('PCP Vector Checklist', () => { 2 | before(() => { 3 | cy.task('grafana:reset'); 4 | cy.login(); 5 | cy.enablePlugin(); 6 | cy.addDatasource('performancecopilot-vector-datasource', 'PCP Vector data source'); 7 | }); 8 | 9 | beforeEach(() => { 10 | cy.login(); 11 | }); 12 | 13 | it('should open the checklist dashboard', () => { 14 | cy.visit('/d/pcp-vector-checklist'); 15 | cy.contains('Memory Utilization'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /cypress/e2e/datasources/valkey.cy.ts: -------------------------------------------------------------------------------- 1 | describe('PCP Valkey data source', () => { 2 | before(() => { 3 | cy.task('grafana:reset'); 4 | cy.login(); 5 | cy.enablePlugin(); 6 | cy.addDatasource('performancecopilot-valkey-datasource', 'PCP Valkey data source'); 7 | }); 8 | 9 | beforeEach(() => { 10 | cy.login(); 11 | }); 12 | 13 | it('should import bundled dashboards', () => { 14 | cy.visit('/datasources'); 15 | cy.contains('PCP Valkey').click(); 16 | cy.contains('Dashboards').click({ force: true }); 17 | cy.contains('td', 'PCP Valkey: Host Overview').siblings().contains('Import').click({ force: true }); 18 | cy.contains('Dashboard Imported'); 19 | }); 20 | 21 | it('should auto-complete metric names', () => { 22 | cy.visit('/dashboard/new'); 23 | 24 | //tests for grafana 10 UI 25 | cy.get('body').then(($body) => { 26 | if ($body.text().search('Add a new panel') > 0) { 27 | cy.contains('Add a new panel').click(); 28 | } else { 29 | cy.contains('Add visualization').click(); 30 | cy.get('div.scrollbar-view').contains('PCP Valkey').click(); 31 | } 32 | }); 33 | 34 | // start typing 35 | cy.get('.monaco-editor textarea').type('disk.dev.by', { force: true }); 36 | cy.contains('disk.dev.total_bytes'); // auto-complete 37 | 38 | // click on one auto-completion entry 39 | cy.contains('disk.dev.write_bytes').click(); 40 | cy.get('.monaco-editor textarea').should('have.value', 'disk.dev.write_bytes').blur(); 41 | 42 | // remove '_bytes' from query editor and type '_' to open auto-completion again 43 | cy.get('.monaco-editor textarea').type('{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}_', { 44 | force: true, 45 | }); 46 | 47 | // click on one auto-completion entry 48 | cy.contains('disk.dev.write_rawactive').click(); 49 | // if the following assertion fails, check the wordPattern setting in the Monaco Editor 50 | cy.get('.monaco-editor textarea').should('have.value', 'disk.dev.write_rawactive').blur(); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /cypress/e2e/datasources/vector.cy.ts: -------------------------------------------------------------------------------- 1 | describe('PCP Vector data source', () => { 2 | before(() => { 3 | cy.task('grafana:reset'); 4 | cy.login(); 5 | cy.enablePlugin(); 6 | cy.addDatasource('performancecopilot-vector-datasource', 'PCP Vector data source'); 7 | }); 8 | 9 | beforeEach(() => { 10 | cy.login(); 11 | }); 12 | 13 | it('should import bundled dashboards', () => { 14 | cy.visit('/datasources'); 15 | cy.contains('PCP Vector').click(); 16 | cy.contains('Dashboards').click({ force: true }); 17 | cy.contains('td', 'PCP Vector: Host Overview').siblings().contains('Import').click({ force: true }); 18 | cy.contains('Dashboard Imported'); 19 | }); 20 | 21 | it('should auto-complete metric names', () => { 22 | cy.visit('/dashboard/new'); 23 | 24 | //test for Grafana 10 UI 25 | cy.get('body').then(($body) => { 26 | if ($body.text().search('Add a new panel') > 0) { 27 | cy.contains('Add a new panel').click(); 28 | } else { 29 | cy.contains('Add visualization').click(); 30 | cy.contains('PCP Vector').click();; 31 | } 32 | }); 33 | 34 | // start typing 35 | cy.get('.monaco-editor').type('disk.dev.write_b'); 36 | // auto-complete 37 | cy.contains('disk.dev.write_bytes'); 38 | cy.contains('Semantics: counter'); 39 | cy.contains('Units: Kbyte'); 40 | cy.contains('per-disk count of bytes written'); 41 | 42 | // accept a suggestion 43 | cy.contains('disk.dev.write_bytes').type('{enter}'); 44 | cy.get('.monaco-editor textarea').should('have.value', 'disk.dev.write_bytes').blur(); 45 | 46 | // remove '_bytes' from query editor and type '_' to open auto-completion again 47 | cy.get('.monaco-editor textarea').type('{backspace}{backspace}{backspace}{backspace}{backspace}{backspace}_', { 48 | force: true, 49 | }); 50 | 51 | // click on one auto-completion entry 52 | cy.contains('disk.dev.write_rawactive').click(); 53 | // if the following assertion fails, check the wordPattern setting in the Monaco Editor 54 | cy.get('.monaco-editor textarea').should('have.value', 'disk.dev.write_rawactive').blur(); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /cypress/e2e/login.cy.ts: -------------------------------------------------------------------------------- 1 | describe('Grafana', () => { 2 | before(() => { 3 | cy.task('grafana:reset'); 4 | }); 5 | 6 | it('login', () => { 7 | cy.visit('/'); 8 | cy.get('[name=user]').type('admin'); 9 | cy.get('[name=password]').type('admin'); 10 | cy.contains('Log in').click(); 11 | cy.contains('Logged in'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /cypress/e2e/setup.cy.ts: -------------------------------------------------------------------------------- 1 | describe('grafana-pcp setup', () => { 2 | before(() => { 3 | cy.task('grafana:reset'); 4 | }); 5 | 6 | beforeEach(() => { 7 | cy.login(); 8 | }); 9 | 10 | it('should install grafana-pcp', () => { 11 | cy.visit('/plugins/performancecopilot-pcp-app/'); 12 | cy.contains('Enable').click(); 13 | cy.get('button').should('include.text', 'Disable'); 14 | }); 15 | 16 | it('should setup PCP Valkey data source', () => { 17 | cy.visit('/datasources/new'); 18 | cy.contains('PCP Valkey').click(); 19 | cy.get('input[placeholder="http://localhost:44322"]').type('http://localhost:44322'); 20 | cy.get('button[type=submit]').click(); 21 | cy.contains('Data source is working'); 22 | }); 23 | 24 | it('should setup PCP Vector data source', () => { 25 | cy.visit('/datasources/new'); 26 | cy.contains('PCP Vector').click(); 27 | cy.get('input[placeholder="http://localhost:44322"]').type('http://localhost:44322'); 28 | cy.get('button[type=submit]').click(); 29 | cy.contains('Data source is working, using Performance Co-Pilot'); 30 | }); 31 | 32 | it('should setup PCP bpftrace data source', () => { 33 | cy.visit('/datasources/new'); 34 | cy.contains('PCP bpftrace').click(); 35 | cy.get('input[placeholder="http://localhost:44322"]').type('http://localhost:44322'); 36 | cy.get('button[type=submit]').click(); 37 | cy.contains('Data source is working, using Performance Co-Pilot'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /cypress/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import * as axios from 'axios'; 2 | import { execSync } from 'child_process'; 3 | 4 | /** 5 | * @type {Cypress.PluginConfig} 6 | */ 7 | export default (on, config) => { 8 | on('task', { 9 | async 'grafana:reset'() { 10 | const resetGrafanaCmd = process.env['RESET_GRAFANA_CMD']; 11 | execSync(resetGrafanaCmd); 12 | 13 | // wait until Grafana is ready 14 | const baseUrl = process.env['CYPRESS_BASE_URL']; 15 | let elapsed = 0; 16 | while (elapsed < 20000) { 17 | try { 18 | await axios.get(baseUrl, { timeout: 500 }); 19 | return true; 20 | } catch { 21 | elapsed += 500; 22 | } 23 | } 24 | return false; 25 | }, 26 | }); 27 | }; 28 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | Cypress.Commands.add('login', (username: string = 'admin', password: string = 'admin') => { 2 | cy.request('POST', '/login', { 3 | user: username, 4 | password, 5 | }); 6 | }); 7 | 8 | Cypress.Commands.add('enablePlugin', () => { 9 | cy.request('POST', '/api/plugins/performancecopilot-pcp-app/settings', { 10 | enabled: true, 11 | pinned: true, 12 | }); 13 | }); 14 | 15 | Cypress.Commands.add('addDatasource', (type: string, name: string, url: string = 'http://localhost:44322') => { 16 | cy.request('POST', '/api/datasources', { 17 | type, 18 | name, 19 | access: 'proxy', 20 | isDefault: true, 21 | url, 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | import './commands'; 2 | 3 | declare global { 4 | namespace Cypress { 5 | interface Chainable { 6 | login(username?: string, password?: string): Chainable; 7 | enablePlugin(): Chainable; 8 | addDatasource(type: string, name: string, url?: string): Chainable; 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /cypress/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["es5", "dom"], 5 | "types": ["cypress"] 6 | }, 7 | "include": ["**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = . 8 | BUILDDIR = _build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /docs/_static/custom.css: -------------------------------------------------------------------------------- 1 | /* allow line breaks in table cells */ 2 | .wy-table-responsive table td { 3 | white-space: normal; 4 | } 5 | -------------------------------------------------------------------------------- /docs/architecture.rst: -------------------------------------------------------------------------------- 1 | .. include:: 2 | 3 | Architecture 4 | ============ 5 | 6 | .. figure:: img/architecture.png 7 | 8 | |copy| Christian Horn 9 | 10 | Monitored Hosts 11 | --------------- 12 | 13 | Monitored hosts run the **Performance Metrics Collector Daemon (PMCD)**, which communicates with one or many **Performance Metrics Domain Agents (PMDAs)** on the same host. 14 | Each **PMDA** is responsible for gathering metrics of one specific domain - e.g., the kernel, services (e.g., PostgreSQL), or other instrumented applications. 15 | The **pmlogger** daemon records metrics from **pmcd** and stores them in archive files on the hard drive. 16 | 17 | Since **PCP 5** metrics can also be stored in the valkey database, which allows multi-host performance analysis, the **pmproxy** daemon discovers new archives (created by **pmlogger**) and stores them in a valkey database. 18 | 19 | Dashboards 20 | ---------- 21 | 22 | Performance Co-Pilot metrics can be analyzed with Grafana dashboards, using the **grafana-pcp** plugin. 23 | There are two modes available: 24 | 25 | * historical metrics across multiple hosts using the :doc:`datasources/valkey` data source 26 | * live, on-host metrics using the :doc:`datasources/vector` data source 27 | 28 | The :doc:`datasources/valkey` data source sends :ref:`pmseries ` queries to **pmproxy**, which in turn queries the valkey database for metrics. 29 | The :doc:`datasources/vector` data source connects to **pmproxy**, which in turn requests live metrics directly from a local or remote **PMCD**. 30 | In this case, metrics are stored temporarily in the browser, and metric values are lost when the browser tab is refreshed. 31 | The :doc:`PCP Valkey data source ` is required for persistence. 32 | -------------------------------------------------------------------------------- /docs/datasources/authentication.rst: -------------------------------------------------------------------------------- 1 | Authentication 2 | ============== 3 | 4 | Performance Co-Pilot supports the following authentication mechanisms through the SASL authentication framework: ``plain``, ``login``, ``digest-md5``, ``scram-sha-256`` and ``gssapi``. 5 | This guide shows how to setup authentication using the ``scram-sha-256`` authentication mechanism and a local user database. 6 | 7 | 8 | .. note:: 9 | Authentication methods ``login``, ``digest-md5`` and ``scram-sha-256`` require PCP 5.1.0 or later. 10 | 11 | Requisites 12 | ---------- 13 | 14 | Install the following package, which provides support for the ``scram-sha-256`` authentication method: 15 | 16 | Fedora/CentOS/RHEL 17 | ^^^^^^^^^^^^^^^^^^ 18 | 19 | .. code-block:: console 20 | 21 | $ sudo dnf install -y cyrus-sasl-scram 22 | 23 | Debian/Ubuntu 24 | ^^^^^^^^^^^^^ 25 | 26 | .. code-block:: console 27 | 28 | $ sudo apt-get install -y libsasl2-modules-gssapi-mit 29 | 30 | Configuring PMCD 31 | ---------------- 32 | 33 | First, open the ``/etc/sasl2/pmcd.conf`` file and specify the supported authentication mechanism and the path to the user database: 34 | 35 | .. code-block:: yaml 36 | 37 | mech_list: scram-sha-256 38 | sasldb_path: /etc/pcp/passwd.db 39 | 40 | Then create a new unix user (in this example ``pcptestuser``) and add it to the user database: 41 | 42 | .. code-block:: console 43 | 44 | $ sudo useradd -r pcptestuser 45 | $ sudo saslpasswd2 -a pmcd pcptestuser 46 | 47 | .. note:: 48 | For every user in the user database, a unix user with the same name must exist. 49 | The passwords of the unix user and the ``/etc/pcp/passwd.db`` database are not synchronized, 50 | and (only) the password of the ``saslpasswd2`` command is used for authentication. 51 | 52 | Make sure that the permissions of the user database are correct (readable only by root and the pcp user): 53 | 54 | .. code-block:: console 55 | 56 | $ sudo chown root:pcp /etc/pcp/passwd.db 57 | $ sudo chmod 640 /etc/pcp/passwd.db 58 | 59 | Finally, restart pmcd and pmproxy: 60 | 61 | .. code-block:: console 62 | 63 | $ sudo systemctl restart pmcd pmproxy 64 | 65 | Test Authentication 66 | ------------------- 67 | 68 | To test if the authentication is set up correctly, execute the following command: 69 | 70 | .. code-block:: console 71 | 72 | $ pminfo -f -h "pcp://127.0.0.1?username=pcptestuser" disk.dev.read 73 | 74 | Configuring the Grafana Data source 75 | ----------------------------------- 76 | 77 | Go to the Grafana data source settings, enable **Basic auth**, and enter the username and password. 78 | Click the *Save & Test* button to check if the authentication is working. 79 | 80 | .. note:: 81 | Due to security reasons, the access mode *Browser* is **not supported** with authentication. 82 | -------------------------------------------------------------------------------- /docs/datasources/bpftrace.rst: -------------------------------------------------------------------------------- 1 | PCP bpftrace 2 | ============ 3 | 4 | bpftrace PMDA installation 5 | -------------------------- 6 | 7 | .. code-block:: console 8 | 9 | $ sudo dnf install pcp-pmda-bpftrace 10 | $ cd /var/lib/pcp/pmdas/bpftrace 11 | $ sudo ./Install 12 | 13 | Query Formats 14 | ------------- 15 | 16 | Time Series 17 | ^^^^^^^^^^^ 18 | Shows bpftrace variables as time series. 19 | For bpftrace maps, each key is shown as a separate target (i.e. line in a line graph), for example ``@counts[comm] = count()``. 20 | If there are multiple variables (or scripts) defined, all values will be combined in the same graph. 21 | 22 | Heatmap 23 | ^^^^^^^ 24 | Transforms bpftrace histograms into heatmaps. 25 | 26 | **The following settings have to be set in the heatmap panel options:** 27 | 28 | ============== ======================= 29 | Setting Value 30 | ============== ======================= 31 | *Format* **Time Series Buckets** 32 | *Bucket bound* **Upper** 33 | ============== ======================= 34 | 35 | Table 36 | ^^^^^ 37 | Transforms CSV output of bpftrace scripts into a table. 38 | The first line must be the column names. 39 | 40 | Legend Format Templating 41 | ------------------------ 42 | The following variables can be used in the legend format box: 43 | 44 | =============== ====================== 45 | Variable Description 46 | =============== ====================== 47 | ``$metric0`` bpftrace variable name 48 | ``$instance`` bpftrace map key 49 | =============== ====================== 50 | 51 | More Information 52 | ---------------- 53 | 54 | `bpftrace PMDA README `_ 55 | -------------------------------------------------------------------------------- /docs/datasources/index.rst: -------------------------------------------------------------------------------- 1 | Overview 2 | ======== 3 | 4 | .. include:: ../refs.rst 5 | 6 | PCP Valkey 7 | --------- 8 | 9 | This data source queries the fast, scalable time series capabilities provided by the `pmseries`_ functionality. 10 | It is intended to query **historical** data across **multiple hosts** and supports filtering based on labels. 11 | 12 | PCP Vector 13 | ---------- 14 | The PCP Vector data source shows **live, on-host metrics** from the real-time `pmwebapi`_ interfaces. 15 | It is intended for an individual host, on-demand performance monitoring, and includes container support. 16 | 17 | PCP bpftrace 18 | ------------ 19 | 20 | The PCP bpftrace data source supports system introspection using `bpftrace`_ scripts. 21 | It connects to the bpftrace PMDA and runs bpftrace scripts on the host. 22 | -------------------------------------------------------------------------------- /docs/datasources/vector.rst: -------------------------------------------------------------------------------- 1 | PCP Vector 2 | ========== 3 | 4 | Query Formats 5 | ------------- 6 | 7 | Time Series 8 | ^^^^^^^^^^^ 9 | Returns the data as time series. 10 | For metrics with instance domains, each instance is shown as a separate target (i.e., line in a line graph). 11 | If there are multiple queries defined, all values will be combined in the same graph. 12 | 13 | Heatmap 14 | ^^^^^^^ 15 | Transforms the data for the heatmap panel. 16 | Instance names have to be in the following format: ``-``, for example, ``512-1023`` (the bcc PMDA produces histograms in this format). 17 | 18 | **The following settings have to be set in the heatmap panel options:** 19 | 20 | ============== ======================= 21 | Setting Value 22 | ============== ======================= 23 | *Format* **Time Series Buckets** 24 | *Bucket bound* **Upper** 25 | ============== ======================= 26 | 27 | Table 28 | ^^^^^ 29 | Transforms the data for the table panel. 30 | Two or more queries are required, and it will transform every metric into a column, and every instance into a row. 31 | The latest values of the currently selected timeframe will be displayed. 32 | 33 | Legend Format Templating 34 | ------------------------ 35 | The following variables can be used in the legend format box: 36 | 37 | =============== ======================== ========================== 38 | Variable Description Example 39 | =============== ======================== ========================== 40 | ``$expr`` query expression ``rate(disk.dm.avactive)`` 41 | ``$metric`` metric name ``disk.dev.read`` 42 | ``$metric0`` last part of metric name ``read`` 43 | ``$instance`` instance name ``sda`` 44 | ``$some_label`` label value anything 45 | =============== ======================== ========================== 46 | -------------------------------------------------------------------------------- /docs/guides/containers.rst: -------------------------------------------------------------------------------- 1 | Monitoring Containers 2 | ===================== 3 | 4 | Importing the dashboards 5 | ------------------------ 6 | 7 | grafana-pcp includes the following (optional) dashboards: 8 | 9 | * PCP Vector: Container Overview (CGroups v1) 10 | * PCP Vector: Container Overview (CGroups v2) 11 | 12 | You can import the corresponding dashboard on the :doc:`../datasources/vector` data source settings page. 13 | 14 | .. note:: 15 | grafana-pcp before version 3.0.0 includes a single dashboard called **PCP Vector: Container Overview** which supports CGroups v1 only and is installed by default (i.e. no import is required). 16 | 17 | Usage 18 | ----- 19 | 20 | You can choose one or multiple containers in the *container* drop-down field at the top of the dashboard: 21 | 22 | .. image:: ../../src/img/screenshots/vector-containers.png 23 | :width: 700 24 | 25 | Common Problems 26 | --------------- 27 | 28 | **My container doesn't show up** 29 | 30 | - make sure that the docker and/or podman PMDAs are installed 31 | - currently PCP only supports containers started by the root user (there is `an open feature request `_ to change this) 32 | -------------------------------------------------------------------------------- /docs/guides/multiple-vector-hosts.rst: -------------------------------------------------------------------------------- 1 | Multiple Vector Hosts 2 | ===================== 3 | 4 | In cloud environments, it is often desired to use the Vector data source to connect to multiple remote hosts without configuring a new data source for each host. 5 | This guide shows a setup for this use case using `Grafana templates `_. 6 | 7 | Setup the Vector data source 8 | ---------------------------- 9 | 10 | Open the Grafana configuration, go to Data Sources, and add the :doc:`../datasources/vector` data source. 11 | Leave the URL field empty and select **Access: Browser**. 12 | Click the save button. A red alert will appear, with the text `To use this data source, please configure the URL in the query editor.` 13 | 14 | Create a new dashboard variable 15 | ------------------------------- 16 | 17 | Create a new dashboard (plus icon in the left navigation - *Create* - *Dashboard*) and open the dashboard settings (wheel icon on the right, top navigation bar). 18 | Navigate to *Variables* and create a new variable with the following settings: 19 | 20 | ======= ======== 21 | Setting Value 22 | ======= ======== 23 | Name host 24 | Type Text box 25 | ======= ======== 26 | 27 | Leave the other fields to their default values. 28 | Save the new variable, go back to the dashboard, enter a hostname (for example, ``localhost``) in the text box, and press enter. 29 | 30 | Create a new graph 31 | ------------------ 32 | 33 | Add a new graph to the dashboard, select the :doc:`../datasources/vector` data source, enter a PCP metric name (for example ``disk.dev.read_bytes``) in the big textbox, and enter ``http://$host:44322`` in the URL field. 34 | If you haven't already, select the time range to *last 5 minutes* and select the auto-refresh interval (top right corner) to 5 seconds, for example. 35 | 36 | Now Grafana connects to ``http://localhost:44322`` for this panel (if you have entered ``localhost`` in the host textbox). By changing the value of the host text box, you can change the remote host. 37 | 38 | Setting the host by query parameter 39 | ----------------------------------- 40 | 41 | You can also set the host by an URL query parameter. 42 | Add ``&var-host=example.com`` to the current query, or update the ``var-host`` query parameter in case it is already present in the current query string. 43 | -------------------------------------------------------------------------------- /docs/img/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/docs/img/architecture.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Performance Co-Pilot Grafana Plugin 2 | =================================== 3 | 4 | .. include:: refs.rst 5 | 6 | `Performance Co-Pilot (PCP) `_ provides a framework and services to support system-level performance monitoring and management. 7 | It presents a unifying abstraction for all of the performance data in a system, and many tools for interrogating, retrieving, and processing that data. 8 | 9 | Features 10 | -------- 11 | 12 | * analysis of historical PCP metrics using `pmseries`_ query language 13 | * analysis of real-time PCP metrics using `pmwebapi`_ live services 14 | * enhanced Berkeley Packet Filter (eBPF) tracing using `bpftrace`_ scripts 15 | * dashboards for detecting potential performance issues and show possible solutions with the checklist dashboards, using the `USE method `_ [2] 16 | * full-text search in metric names, descriptions, instances [1] 17 | * support for `Grafana Alerting `_ [1] 18 | * support for `derived metrics `_ (allows the usage of arithmetic operators and statistical functions inside a query) [2] 19 | * automated configuration of metric units [1,2,3] 20 | * automatic rate and time utilization conversion 21 | * heatmap, table [2,3] and flame graph [3] support 22 | * auto-completion of metric names [1,2], qualifier keys and values [1], and bpftrace probes, builtin variables and functions [3] 23 | * display of semantics, units and help texts of metrics [2] and bpftrace builtins [3] 24 | * legend templating support with ``$metric``, ``$metric0``, ``$instance``, ``$some_label``, ``$some_dashboard_variable`` 25 | * container support [1,2] 26 | * support for custom endpoint and hostspec per panel [2,3] 27 | * support for repeated panels 28 | * sample dashboards for all data sources 29 | 30 | [1] PCP Valkey 31 | [2] PCP Vector 32 | [3] PCP bpftrace 33 | 34 | Getting started 35 | --------------- 36 | 37 | * :doc:`quickstart` 38 | * :doc:`installation` 39 | 40 | .. toctree:: 41 | :caption: Getting started 42 | :hidden: 43 | 44 | quickstart 45 | installation 46 | screenshots 47 | architecture 48 | CHANGELOG 49 | 50 | .. toctree:: 51 | :caption: Data Sources 52 | :hidden: 53 | :titlesonly: 54 | 55 | datasources/index 56 | 57 | .. toctree:: 58 | :hidden: 59 | 60 | datasources/authentication 61 | datasources/valkey 62 | datasources/vector 63 | datasources/bpftrace 64 | 65 | .. toctree:: 66 | :caption: Guides 67 | :hidden: 68 | 69 | guides/multiple-vector-hosts.rst 70 | guides/containers.rst 71 | 72 | .. toctree:: 73 | :hidden: 74 | :caption: Troubleshooting 75 | 76 | troubleshooting 77 | -------------------------------------------------------------------------------- /docs/pcp-valkey.md: -------------------------------------------------------------------------------- 1 | # Moved 2 | 3 | This document has moved to https://grafana-pcp.readthedocs.io/en/latest/datasources/valkey.html 4 | -------------------------------------------------------------------------------- /docs/quickstart.rst: -------------------------------------------------------------------------------- 1 | Quickstart 2 | ========== 3 | 4 | .. include:: refs.rst 5 | 6 | Installation 7 | ------------ 8 | 9 | Please see the :doc:`Installation Guide `. There is a simple method using the 10 | package manager for Red Hat-based distributions, otherwise it can be installed from source, from 11 | a pre-built plugin bundle from the project's GitHub releases page, or as a container. 12 | 13 | Make sure to restart Grafana server and pmproxy after installation the plugin. Eg. 14 | 15 | .. code-block:: console 16 | 17 | $ sudo systemctl restart grafana-server 18 | $ sudo systemctl start pmproxy 19 | 20 | Installation is not finished until you also enable the Performance Co-Pilot plugin via 21 | the Grafana Admin configuration: 22 | 23 | Open the Grafana configuration, go to Plugins, select *Performance Co-Pilot* and click the *Enable* button on it's page. This will make the PCP data sources and some dashboards available. 24 | 25 | Data Sources 26 | ------------ 27 | 28 | Before using grafana-pcp, you need to configure the data sources. 29 | Open the Grafana configuration, go to Data Sources and add the 30 | :doc:`datasources/valkey`, 31 | :doc:`datasources/vector` and/or 32 | :doc:`datasources/bpftrace` data sources. 33 | 34 | The only required configuration field for each data source is the URL to `pmproxy`_. 35 | In most cases the default URL ``http://localhost:44322`` can be used. 36 | All other fields can be left to their default values. 37 | 38 | Each data source includes one or more pre-defined dashboards. 39 | You can import them by navigating to the *Dashboards* tab on top of the settings and clicking the *Import* button next to the dashboard name. 40 | 41 | .. note:: 42 | Make sure the *URL* text box actually contains a value (font color should be white) and you're not looking at the placeholder value (light grey text). 43 | 44 | .. note:: 45 | The Valkey and bpftrace data sources need additional configuration on the collector host. 46 | See :doc:`datasources/valkey` and :doc:`datasources/bpftrace`. 47 | 48 | Dashboards 49 | ---------- 50 | 51 | After installing grafana-pcp and configuring the data sources, you're ready to open the pre-defined dashboards (see above) or create new ones. 52 | Each data source comes with a few pre-defined dashboards, showing most of the respective functionality. 53 | Further information on each data source and the functionality can be found in the :doc:`Data Sources ` section. 54 | -------------------------------------------------------------------------------- /docs/refs.rst: -------------------------------------------------------------------------------- 1 | .. _pmlogger: https://man7.org/linux/man-pages/man1/pmlogger.1.html 2 | .. _pmproxy: https://man7.org/linux/man-pages/man1/pmproxy.1.html 3 | .. _pmseries: https://man7.org/linux/man-pages/man1/pmseries.1.html 4 | .. _pmwebapi: https://man7.org/linux/man-pages/man3/pmwebapi.3.html 5 | .. _bpftrace: https://github.com/iovisor/bpftrace/blob/master/README.md 6 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx-rtd-theme>=0.5.1 2 | recommonmark 3 | -------------------------------------------------------------------------------- /docs/screenshots.rst: -------------------------------------------------------------------------------- 1 | Screenshots 2 | =========== 3 | 4 | PCP Vector 5 | ---------- 6 | 7 | .. image:: ../src/img/screenshots/vector-overview.png 8 | :width: 700 9 | .. image:: ../src/img/screenshots/vector-metric-autocompletion.png 10 | :width: 700 11 | 12 | Vector dashboards 13 | ~~~~~~~~~~~~~~~~~ 14 | 15 | .. image:: ../src/img/screenshots/vector-containers.png 16 | :width: 700 17 | .. image:: ../src/img/screenshots/vector-bcc-overview1.png 18 | :width: 700 19 | .. image:: ../src/img/screenshots/vector-bcc-overview2.png 20 | :width: 700 21 | .. image:: ../src/img/screenshots/vector-checklist.png 22 | :width: 700 23 | 24 | PCP bpftrace 25 | ------------ 26 | 27 | .. image:: ../src/img/screenshots/bpftrace-cpu.png 28 | :width: 700 29 | .. image:: ../src/img/screenshots/bpftrace-disk.png 30 | :width: 700 31 | .. image:: ../src/img/screenshots/bpftrace-tcp.png 32 | :width: 700 33 | 34 | bpftrace code editor 35 | ~~~~~~~~~~~~~~~~~~~~ 36 | 37 | .. image:: ../src/img/screenshots/bpftrace-probe-autocompletion.png 38 | :width: 700 39 | .. image:: ../src/img/screenshots/bpftrace-variable-autocompletion.png 40 | :width: 700 41 | .. image:: ../src/img/screenshots/bpftrace-function-autocompletion.png 42 | :width: 700 43 | 44 | bpftrace flame graphs 45 | ~~~~~~~~~~~~~~~~~~~~~ 46 | 47 | .. image:: ../src/img/screenshots/bpftrace-flame-graph.png 48 | :width: 700 49 | 50 | Metric Search 51 | ------------- 52 | 53 | .. image:: ../src/img/screenshots/search.png 54 | :width: 700 55 | -------------------------------------------------------------------------------- /docs/troubleshooting.rst: -------------------------------------------------------------------------------- 1 | Troubleshooting 2 | =============== 3 | 4 | .. include:: refs.rst 5 | 6 | Common Problems 7 | --------------- 8 | 9 | HTTP Error 502: Bad Gateway, please check the datasource and pmproxy settings 10 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 11 | 12 | **When I try to add a data source in Grafana, I get the following error:** 13 | **"HTTP Error 502: Bad Gateway, please check the datasource and pmproxy settings. To use this data source, please configure the URL in the query editor."** 14 | 15 | * check if pmproxy is running: ``systemctl status pmproxy`` 16 | * make sure that pmproxy was built with time-series (libuv) support enabled. You can verify that by reading the logfile in ``/var/log/pcp/pmproxy/pmproxy.log`` 17 | 18 | 19 | PCP Valkey 20 | --------- 21 | 22 | Grafana doesn't show any data 23 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 24 | 25 | * Make sure that `pmlogger`_ is up and running, and writing archives to the disk (``/var/log/pcp/pmlogger//*``) 26 | * Verify that `pmproxy`_ is running, time series support is enabled and a connection to Valkey is established: check the logfile at ``/var/log/pcp/pmproxy/pmproxy.log`` and make sure that it contains the following text: ``Info: Valkey slots, command keys, schema version setup`` 27 | * Check if the Valkey database contains any keys: ``valkey-cli dbsize`` 28 | * Check if any PCP metrics are in the Valkey database: ``pmseries disk.dev.read`` 29 | * Check if PCP metric values are in the Valkey database: ``pmseries 'disk.dev.read[count:10]'`` 30 | * Check the Grafana logs: ``journalctl -e -u grafana-server`` 31 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // This file is needed because it is used by vscode and other tools that 2 | // call `jest` directly. However, unless you are doing anything special 3 | // do not edit this file 4 | 5 | const standard = require('@grafana/toolkit/src/config/jest.plugin.config'); 6 | 7 | // This process will use the same config that `yarn test` is using 8 | const defaultConfig = standard.jestConfig(); 9 | module.exports = { 10 | ...defaultConfig, 11 | moduleNameMapper: { 12 | ...defaultConfig.moduleNameMapper, 13 | // workaround for: 14 | // @grafana/toolkit/src/config/react-inlinesvg.tsx:5 15 | // SyntaxError: Cannot use import statement outside a module 16 | 'react-inlinesvg': undefined, 17 | }, 18 | transformIgnorePatterns: [ 19 | ...defaultConfig.transformIgnorePatterns, 20 | // required for d3-flame-graph (uses ES6 syntax) 21 | '/node_modules/(?!d3-flame-graph/)', 22 | ], 23 | }; 24 | -------------------------------------------------------------------------------- /jsonnetfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/grafana/grafonnet-lib.git", 8 | "subdir": "grafonnet" 9 | } 10 | }, 11 | "version": "master" 12 | } 13 | ], 14 | "legacyImports": true 15 | } 16 | -------------------------------------------------------------------------------- /jsonnetfile.lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "dependencies": [ 4 | { 5 | "source": { 6 | "git": { 7 | "remote": "https://github.com/grafana/grafonnet-lib.git", 8 | "subdir": "grafonnet" 9 | } 10 | }, 11 | "version": "3626fc4dc2326931c530861ac5bebe39444f6cbf", 12 | "sum": "gF8foHByYcB25jcUOBqP6jxk0OPifQMjPvKY0HaCk6w=" 13 | } 14 | ], 15 | "legacyImports": false 16 | } 17 | -------------------------------------------------------------------------------- /pkg/datasources/valkey/api/pmseries/models.go: -------------------------------------------------------------------------------- 1 | package pmseries 2 | 3 | type GenericSuccessResponse struct { 4 | Success bool `json:"success"` 5 | } 6 | 7 | type GenericErrorResponse struct { 8 | Success bool `json:"success"` 9 | Message string `json:"message"` 10 | } 11 | 12 | type QueryResponse []string 13 | 14 | type MetricsResponseItem struct { 15 | Series string `json:"series"` 16 | Name string `json:"name"` 17 | } 18 | 19 | type MetricNamesResponse []string 20 | 21 | type DescsResponseItem struct { 22 | Series string `json:"series"` 23 | Source string `json:"source"` 24 | PMID string `json:"pmid"` 25 | Indom string `json:"indom"` 26 | Semantics string `json:"semantics"` 27 | Type string `json:"type"` 28 | Units string `json:"units"` 29 | } 30 | 31 | type InstancesResponseItem struct { 32 | Series string `json:"series"` 33 | Source string `json:"source"` 34 | Instance string `json:"instance"` 35 | ID int `json:"id"` 36 | Name string `json:"name"` 37 | } 38 | 39 | type LabelsResponseItem struct { 40 | Series string `json:"series"` 41 | Labels map[string]interface{} `json:"labels"` 42 | } 43 | 44 | type LabelNamesResponse []string 45 | 46 | type LabelValuesResponse map[string][]interface{} 47 | 48 | type ValuesResponseItem struct { 49 | Series string `json:"series"` 50 | Timestamp float64 `json:"timestamp"` // milliseconds 51 | Instance string `json:"instance"` // can be empty 52 | Value string `json:"value"` 53 | } 54 | -------------------------------------------------------------------------------- /pkg/datasources/valkey/models.go: -------------------------------------------------------------------------------- 1 | package valkey 2 | 3 | type TargetFormat string 4 | 5 | const ( 6 | TimeSeries TargetFormat = "time_series" 7 | Heatmap TargetFormat = "heatmap" 8 | Geomap TargetFormat = "geomap" 9 | ) 10 | 11 | // Query is a single valkey query (target) 12 | type Query struct { 13 | Expr string `json:"expr"` 14 | Format TargetFormat `json:"format"` 15 | LegendFormat string `json:"legendFormat"` 16 | Options QueryOptions `json:"options"` 17 | } 18 | 19 | // QueryOptions are optional query options 20 | type QueryOptions struct { 21 | RateConversion bool `json:"rateConversion"` 22 | TimeUtilizationConversion bool `json:"timeUtilizationConversion"` 23 | } 24 | 25 | func DefaultQuery() Query { 26 | return Query{ 27 | Format: TimeSeries, 28 | Options: QueryOptions{ 29 | RateConversion: true, 30 | TimeUtilizationConversion: true, 31 | }, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /pkg/datasources/valkey/resource/metric_find_value.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | 7 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 8 | ) 9 | 10 | var metricNamesRegex = regexp.MustCompile(`^metrics\(\s*([\w.*]*)\s*\)$`) 11 | var labelNamesRegex = regexp.MustCompile(`^label_names\(\s*([\w.]*)\s*\)$`) 12 | var labelValuesRegex = regexp.MustCompile(`^label_values\(\s*([\w.]+)\s*\)$`) 13 | 14 | func (rs *Service) getMetricNames(pattern string) ([]MetricFindValue, error) { 15 | if pattern == "" { 16 | pattern = "*" 17 | } 18 | 19 | namesResponse, err := rs.pmseriesAPI.MetricNames(pattern) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | metricFindValues := []MetricFindValue{} 25 | for _, name := range namesResponse { 26 | metricFindValues = append(metricFindValues, MetricFindValue{name}) 27 | } 28 | return metricFindValues, nil 29 | } 30 | 31 | func (rs *Service) getLabelNames(pattern string) ([]MetricFindValue, error) { 32 | if pattern == "" { 33 | pattern = "*" 34 | } 35 | 36 | labelNamesResponse, err := rs.pmseriesAPI.LabelNames(pattern) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | ret := []MetricFindValue{} 42 | for _, name := range labelNamesResponse { 43 | ret = append(ret, MetricFindValue{name}) 44 | } 45 | return ret, nil 46 | } 47 | 48 | func (rs *Service) getLabelValues(labelName string) ([]MetricFindValue, error) { 49 | labelValuesResponse, err := rs.pmseriesAPI.LabelValues([]string{labelName}) 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | ret := []MetricFindValue{} 55 | for _, value := range labelValuesResponse[labelName] { 56 | ret = append(ret, MetricFindValue{fmt.Sprintf("%v", value)}) 57 | } 58 | return ret, nil 59 | } 60 | 61 | func (rs *Service) metricFindQuery(query string) ([]MetricFindValue, error) { 62 | log.DefaultLogger.Debug("metricFindQuery", "query", query) 63 | 64 | metricNamesQuery := metricNamesRegex.FindStringSubmatch(query) 65 | if len(metricNamesQuery) == 2 { 66 | return rs.getMetricNames(metricNamesQuery[1]) 67 | } 68 | 69 | labelNamesQuery := labelNamesRegex.FindStringSubmatch(query) 70 | if len(labelNamesQuery) == 2 { 71 | return rs.getLabelNames(labelNamesQuery[1]) 72 | } 73 | 74 | labelValuesQuery := labelValuesRegex.FindStringSubmatch(query) 75 | if len(labelValuesQuery) == 2 { 76 | return rs.getLabelValues(labelValuesQuery[1]) 77 | } 78 | 79 | return []MetricFindValue{}, nil 80 | } 81 | -------------------------------------------------------------------------------- /pkg/datasources/valkey/resource/models.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | // MetricFindValue is the response for metricFindQuery 4 | type MetricFindValue struct { 5 | Text string `json:"text"` 6 | } 7 | -------------------------------------------------------------------------------- /pkg/datasources/valkey/resource/resource.go: -------------------------------------------------------------------------------- 1 | package resource 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/performancecopilot/grafana-pcp/pkg/datasources/valkey/api/pmseries" 8 | ) 9 | 10 | type Service struct { 11 | pmseriesAPI pmseries.API 12 | } 13 | 14 | // NewResourceService creates a new resource service 15 | func NewResourceService(pmseriesAPI pmseries.API) *Service { 16 | return &Service{pmseriesAPI} 17 | } 18 | 19 | func (rs *Service) CallResource(method string, queryParams url.Values) (interface{}, error) { 20 | switch method { 21 | case "metricFindQuery": 22 | query, ok := queryParams["query"] 23 | if !ok || len(query) != 1 { 24 | return nil, fmt.Errorf("invalid query passed to metricFindQuery") 25 | } 26 | return rs.metricFindQuery(query[0]) 27 | default: 28 | return nil, fmt.Errorf("unknown method '%s'", method) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /pkg/datasources/valkey/series/models.go: -------------------------------------------------------------------------------- 1 | package series 2 | 3 | import "github.com/performancecopilot/grafana-pcp/pkg/datasources/valkey/api/pmseries" 4 | 5 | // Series is a single time series with descriptor, labels and instances, but without values 6 | type Series struct { 7 | MetricName string 8 | Desc Desc 9 | Labels Labels 10 | Instances map[string]Instance 11 | } 12 | 13 | // Desc describes a metric 14 | type Desc = pmseries.DescsResponseItem 15 | 16 | // Instance holds information about a PCP instance 17 | type Instance struct { 18 | Instance string 19 | Name string 20 | Labels Labels 21 | } 22 | 23 | // Labels structure for storing labels 24 | type Labels map[string]interface{} 25 | -------------------------------------------------------------------------------- /pkg/datasources/valkey/test/fixtures/pmseriesf/fixtures.go: -------------------------------------------------------------------------------- 1 | package pmseriesf 2 | 3 | import ( 4 | "github.com/performancecopilot/grafana-pcp/pkg/datasources/valkey/api/pmseries" 5 | "github.com/performancecopilot/grafana-pcp/pkg/datasources/valkey/series" 6 | ) 7 | 8 | var metrics = map[string]series.Series{ 9 | "f87250c4ea0e5eca8ff2ca3b3044ba1a6c91a3d9": series.Series{ 10 | MetricName: "disk.dev.read", 11 | Desc: series.Desc{ 12 | Series: "f87250c4ea0e5eca8ff2ca3b3044ba1a6c91a3d9", 13 | Source: "2914f38f7bdcb7fb3ac0b822c98019248fd541fb", 14 | PMID: "60.0.4", 15 | Indom: "60.1", 16 | Semantics: "counter", 17 | Type: "u64", 18 | Units: "count", 19 | }, 20 | Labels: series.Labels{}, 21 | Instances: map[string]series.Instance{ 22 | "0aeab8b239522ab0640577ed788cc601fc640266": series.Instance{ 23 | Instance: "0aeab8b239522ab0640577ed788cc601fc640266", 24 | Name: "sda", 25 | Labels: series.Labels{ 26 | "indom_name": "per disk", 27 | "device_type": "block", 28 | "agent": "linux", 29 | "userid": 978, 30 | "machineid": "6dabb302d60b402dabcc13dc4fd0fab8", 31 | "hostname": "dev", 32 | "groupid": 976, 33 | "domainname": "localdomain", 34 | }, 35 | }, 36 | "7f3afb6f41e53792b18e52bcec26fdfa2899fa58": series.Instance{ 37 | Instance: "7f3afb6f41e53792b18e52bcec26fdfa2899fa58", 38 | Name: "nvme0n1", 39 | Labels: series.Labels{ 40 | "indom_name": "per disk", 41 | "device_type": "block", 42 | "agent": "linux", 43 | "userid": 978, 44 | "machineid": "6dabb302d60b402dabcc13dc4fd0fab8", 45 | "hostname": "dev", 46 | "groupid": 976, 47 | "domainname": "localdomain", 48 | }, 49 | }, 50 | }, 51 | }, 52 | } 53 | 54 | var values = map[string][]pmseries.ValuesResponseItem{ 55 | "f87250c4ea0e5eca8ff2ca3b3044ba1a6c91a3d9": []pmseries.ValuesResponseItem{ 56 | pmseries.ValuesResponseItem{ 57 | Series: "f87250c4ea0e5eca8ff2ca3b3044ba1a6c91a3d9", 58 | Instance: "7f3afb6f41e53792b18e52bcec26fdfa2899fa58", 59 | Timestamp: 1599320691309.872, 60 | Value: "100", 61 | }, 62 | pmseries.ValuesResponseItem{ 63 | Series: "f87250c4ea0e5eca8ff2ca3b3044ba1a6c91a3d9", 64 | Instance: "0aeab8b239522ab0640577ed788cc601fc640266", 65 | Timestamp: 1599320691309.872, 66 | Value: "200", 67 | }, 68 | pmseries.ValuesResponseItem{ 69 | Series: "f87250c4ea0e5eca8ff2ca3b3044ba1a6c91a3d9", 70 | Instance: "7f3afb6f41e53792b18e52bcec26fdfa2899fa58", 71 | Timestamp: 1599320692309.872, 72 | Value: "300", 73 | }, 74 | pmseries.ValuesResponseItem{ 75 | Series: "f87250c4ea0e5eca8ff2ca3b3044ba1a6c91a3d9", 76 | Instance: "0aeab8b239522ab0640577ed788cc601fc640266", 77 | Timestamp: 1599320692309.872, 78 | Value: "500", 79 | }, 80 | }, 81 | } 82 | -------------------------------------------------------------------------------- /pkg/datasources/valkey/test/fixtures/pmseriesf/generators.go: -------------------------------------------------------------------------------- 1 | package pmseriesf 2 | 3 | import "github.com/performancecopilot/grafana-pcp/pkg/datasources/valkey/api/pmseries" 4 | 5 | func Query(sids []string) pmseries.QueryResponse { 6 | return sids 7 | } 8 | 9 | func Metrics(sids []string) []pmseries.MetricsResponseItem { 10 | r := []pmseries.MetricsResponseItem{} 11 | for _, sid := range sids { 12 | r = append(r, pmseries.MetricsResponseItem{Series: sid, Name: metrics[sid].MetricName}) 13 | } 14 | return r 15 | } 16 | 17 | func Descs(sids []string) []pmseries.DescsResponseItem { 18 | r := []pmseries.DescsResponseItem{} 19 | for _, sid := range sids { 20 | r = append(r, metrics[sid].Desc) 21 | } 22 | return r 23 | } 24 | 25 | func Instances(sids []string) []pmseries.InstancesResponseItem { 26 | r := []pmseries.InstancesResponseItem{} 27 | for _, sid := range sids { 28 | for _, instance := range metrics[sid].Instances { 29 | r = append(r, pmseries.InstancesResponseItem{ 30 | Series: sid, 31 | Instance: instance.Instance, 32 | Name: instance.Name, 33 | }) 34 | } 35 | } 36 | return r 37 | } 38 | 39 | func Values(sids []string) []pmseries.ValuesResponseItem { 40 | r := []pmseries.ValuesResponseItem{} 41 | for _, sid := range sids { 42 | r = append(r, values[sid]...) 43 | } 44 | return r 45 | } 46 | 47 | func Labels(sids []string) []pmseries.LabelsResponseItem { 48 | r := []pmseries.LabelsResponseItem{} 49 | for _, sid := range sids { 50 | for _, metric := range metrics { 51 | instance, ok := metric.Instances[sid] 52 | if ok { 53 | r = append(r, pmseries.LabelsResponseItem{ 54 | Series: sid, 55 | Labels: instance.Labels, 56 | }) 57 | } 58 | } 59 | } 60 | return r 61 | } 62 | -------------------------------------------------------------------------------- /pkg/datasources/valkey/utils.go: -------------------------------------------------------------------------------- 1 | package valkey 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/grafana/grafana-plugin-sdk-go/data" 7 | ) 8 | 9 | func prettyPrintDataFrame(frame *data.Frame) string { 10 | ret := fmt.Sprintf("Frame name=%s\n", frame.Name) 11 | for i := 0; i < frame.Fields[0].Len(); i++ { 12 | ret += fmt.Sprintf("i=%d", i) 13 | for _, field := range frame.Fields { 14 | ret += fmt.Sprintf("%s=%v", field.Name, field.At(i)) 15 | } 16 | ret += "\n" 17 | } 18 | return ret 19 | } 20 | -------------------------------------------------------------------------------- /pkg/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" 7 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 8 | "github.com/performancecopilot/grafana-pcp/pkg/datasources/valkey" 9 | ) 10 | 11 | func main() { 12 | // Start listening to requests send from Grafana. This call is blocking so 13 | // it wont finish until Grafana shutsdown the process or the plugin choose 14 | // to exit close down by itself 15 | err := datasource.Serve(valkey.NewDatasource()) 16 | 17 | // Log any error if we could start the plugin. 18 | if err != nil { 19 | log.DefaultLogger.Error(err.Error()) 20 | os.Exit(1) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/common/services/pmsearch/types.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceInstanceSettings } from '@grafana/data'; 2 | 3 | export interface PmSearchApiConfig { 4 | dsInstanceSettings: DataSourceInstanceSettings; 5 | timeoutMs: number; 6 | baseUrl: string; 7 | } 8 | 9 | export enum SearchEntity { 10 | None = 0, 11 | Metrics = 1 << 0, 12 | InstanceDomains = 1 << 1, 13 | Instances = 1 << 2, 14 | All = Metrics | InstanceDomains | Instances, 15 | } 16 | 17 | export enum EntityType { 18 | Metric = 'metric', 19 | Instance = 'instance', 20 | InstanceDomain = 'indom', 21 | } 22 | 23 | export interface AutocompleteQueryParams { 24 | query: string; 25 | limit?: number; 26 | } 27 | 28 | export type AutocompleteSuggestion = string; 29 | 30 | export type AutocompleteResponse = AutocompleteSuggestion[]; 31 | 32 | export interface TextQueryParams { 33 | query: string; 34 | highlight?: Array>; 35 | offset?: number; 36 | limit?: number; 37 | field?: Array>; 38 | return?: TextItemResponseField[]; 39 | type?: SearchEntity; 40 | } 41 | 42 | export enum TextItemResponseField { 43 | Type = 'type', 44 | Name = 'name', 45 | Indom = 'indom', 46 | Oneline = 'oneline', 47 | Helptext = 'helptext', 48 | } 49 | 50 | export interface TextItemResponse { 51 | /* All the ones below may be omited when they are filtered out by ?return param, or whey they lack any value (helptexts for example) */ 52 | name?: string; // name field 53 | type?: EntityType; // type field (we always have only single type value on any record 54 | indom?: string; // indom field 55 | oneline?: string; // oneline field 56 | helptext?: string; // helptext field 57 | } 58 | 59 | export interface TextResponse { 60 | total: number; // ValkeySearch returns total number of matching records even if results themselves are limited 61 | elapsed: number; 62 | limit: number; 63 | offset: number; 64 | results: TextItemResponse[]; 65 | } 66 | 67 | export interface IndomQueryParams { 68 | query: string; 69 | limit?: number; 70 | offset?: number; 71 | } 72 | 73 | export class SearchNotAvailableError extends Error { 74 | constructor(message?: string) { 75 | super( 76 | message ?? `Metric Search not available. Please install the ValkeySearch Valkey module and restart pmproxy.` 77 | ); 78 | Object.setPrototypeOf(this, new.target.prototype); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/common/types/errors.ts: -------------------------------------------------------------------------------- 1 | import { has } from 'lodash'; 2 | 3 | export class GenericError extends Error { 4 | err?: GenericError; 5 | 6 | constructor(message: string, err?: GenericError) { 7 | super(message); 8 | this.err = err; 9 | Object.setPrototypeOf(this, new.target.prototype); 10 | } 11 | } 12 | 13 | export class NetworkError extends GenericError { 14 | data?: { message?: string }; 15 | 16 | constructor(error: any) { 17 | let message; 18 | 19 | /** 20 | * error has the following attributes: 21 | * mode with server response without server response 22 | * browser status, statusText, data message, data: {message: 'unexpected error'} 23 | * proxy status, statusText, data status, statusText, data: {error, response, message: 'Bad Gateway'} 24 | */ 25 | 26 | if (error instanceof TypeError) { 27 | // browser mode, no server response 28 | message = `Network Error: ${error.message}`; 29 | } else if (error.status === 502) { 30 | // most likely proxy mode, no server response 31 | // could also be pmproxy returning 502, but unlikely 32 | message = `Network Error: ${error.statusText}`; 33 | } else if (has(error, 'data.message')) { 34 | // pmproxy returned an error message 35 | message = error.data.message; 36 | } else { 37 | // pmproxy didn't return an error message 38 | message = `HTTP Error ${error.status}: ${error.statusText}`; 39 | } 40 | 41 | super(message); 42 | if ('data' in error) { 43 | this.data = error.data; 44 | } 45 | Object.setPrototypeOf(this, new.target.prototype); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/common/types/pcp.ts: -------------------------------------------------------------------------------- 1 | import { Dict } from './utils'; 2 | 3 | export type MetricName = string; 4 | export type InstanceName = string; 5 | 6 | export enum Semantics { 7 | Instant = 'instant', 8 | Discrete = 'discrete', 9 | Counter = 'counter', 10 | } 11 | 12 | export type Labels = Dict; 13 | -------------------------------------------------------------------------------- /src/common/types/utils.ts: -------------------------------------------------------------------------------- 1 | export type Dict = { 2 | [P in K]?: T; 3 | }; 4 | -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- 1 | import { isString } from 'lodash'; 2 | import rootLogger, { LogLevelDesc } from 'loglevel'; 3 | import logPrefixer from 'loglevel-plugin-prefix'; 4 | import { Required } from 'utility-types'; 5 | import { DataSourceInstanceSettings } from '@grafana/data'; 6 | import { BackendSrvRequest } from '@grafana/runtime'; 7 | import { GenericError } from './types/errors'; 8 | 9 | rootLogger.setDefaultLevel('INFO'); 10 | logPrefixer.reg(rootLogger); 11 | logPrefixer.apply(rootLogger, { template: '[%t] %l %n:' }); 12 | 13 | export function setGlobalLogLevel(level: LogLevelDesc) { 14 | for (const logger of Object.values(rootLogger.getLoggers())) { 15 | logger.setDefaultLevel(level); 16 | } 17 | } 18 | 19 | export type DefaultRequestOptions = Omit; 20 | export function getRequestOptions(instanceSettings: DataSourceInstanceSettings): DefaultRequestOptions { 21 | const defaultRequestOptions: Required = { 22 | headers: { 23 | 'Content-Type': 'application/json', 24 | }, 25 | showSuccessAlert: false, 26 | }; 27 | if (instanceSettings.basicAuth || instanceSettings.withCredentials) { 28 | defaultRequestOptions.withCredentials = true; 29 | } 30 | if (instanceSettings.basicAuth) { 31 | defaultRequestOptions.headers['Authorization'] = instanceSettings.basicAuth; 32 | } 33 | return defaultRequestOptions; 34 | } 35 | 36 | export class TimeoutError extends GenericError { 37 | constructor(message?: string, err?: GenericError) { 38 | super(message ?? 'request timeout', err); 39 | Object.setPrototypeOf(this, new.target.prototype); 40 | } 41 | } 42 | 43 | export function timeout(promise: Promise, ms: number): Promise { 44 | return new Promise(async (resolve, reject) => { 45 | const timeout = setTimeout(() => { 46 | reject(new TimeoutError()); 47 | }, ms); 48 | try { 49 | const result = await promise; 50 | clearTimeout(timeout); 51 | resolve(result); 52 | } catch (e) { 53 | clearTimeout(timeout); 54 | reject(e); 55 | } 56 | }); 57 | } 58 | 59 | export function isBlank(str?: string) { 60 | return !(str && isString(str) && str.trim().length > 0); 61 | } 62 | 63 | export function interval_to_ms(str: string) { 64 | if (str.length === 0) { 65 | return 0; 66 | } 67 | 68 | const suffix = str.substring(str.length - 1); 69 | if (suffix === 's') { 70 | return parseInt(str.substring(0, str.length - 1), 10) * 1000; 71 | } else if (suffix === 'm') { 72 | return parseInt(str.substring(0, str.length - 1), 10) * 1000 * 60; 73 | } else if (suffix === 'h') { 74 | return parseInt(str.substring(0, str.length - 1), 10) * 1000 * 60 * 60; 75 | } else { 76 | return parseInt(str, 10) * 1000; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/components/app/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppRootProps } from '@grafana/data'; 3 | import { css } from '@emotion/css'; 4 | 5 | export function App(props: AppRootProps) { 6 | return ( 7 |
8 |

Performance Co-Pilot App

9 | This app integrates metrics from Performance Co-Pilot. 10 |
11 |
12 | It includes the following data sources: 13 |
    18 |
  • 19 | PCP Valkey for fast, scalable time series aggregation across multiple hosts 20 |
  • 21 |
  • 22 | PCP Vector for live, on-host metrics analysis, with container support 23 |
  • 24 |
  • 25 | PCP bpftrace for system introspection using bpftrace scripts 26 |
  • 27 |
28 |
29 | ); 30 | } 31 | -------------------------------------------------------------------------------- /src/components/appconfig/types.ts: -------------------------------------------------------------------------------- 1 | export interface AppSettings {} 2 | -------------------------------------------------------------------------------- /src/components/monaco/MonacoEditorLazy.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useAsync } from 'react-use'; 3 | import { ErrorWithStack, LoadingPlaceholder } from '@grafana/ui'; 4 | import { MonacoEditorWrapperProps } from './MonacoEditorWrapper'; 5 | 6 | // COPY FROM https://github.com/grafana/grafana/blob/master/packages/grafana-ui/src/components/Monaco/CodeEditorLazy.tsx 7 | // duplication is required to get access to the monaco object 8 | export const MonacoEditorLazy: React.FC = props => { 9 | const { loading, error, value } = useAsync(async () => { 10 | return await import(/* webpackChunkName: "monaco-editor" */ './MonacoEditorWrapper'); 11 | }); 12 | 13 | if (loading) { 14 | return ; 15 | } 16 | 17 | if (error) { 18 | return ( 19 | 24 | ); 25 | } 26 | 27 | const MonacoEditor = (value as any).default; 28 | return ; 29 | }; 30 | -------------------------------------------------------------------------------- /src/components/monaco/index.ts: -------------------------------------------------------------------------------- 1 | import type * as MonacoType from 'monaco-editor/esm/vs/editor/editor.api'; 2 | 3 | export type { MonacoType }; 4 | export declare type Monaco = typeof MonacoType; 5 | -------------------------------------------------------------------------------- /src/components/search/App.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import React from 'react'; 3 | import { App, AppProps } from './App'; 4 | import { ViewState } from './store/slices/search/slices/view/state'; 5 | 6 | describe('', () => { 7 | let appProps: AppProps; 8 | 9 | beforeEach(() => { 10 | appProps = { view: ViewState.Index }; 11 | }); 12 | 13 | test('renders without crashing', () => { 14 | shallow(); 15 | }); 16 | 17 | test('can render detail page', () => { 18 | const wrapper = shallow(); 19 | expect(wrapper.exists('[data-test="detail-page"]')).toBe(true); 20 | }); 21 | 22 | test('can render search page', () => { 23 | const wrapper = shallow(); 24 | expect(wrapper.exists('[data-test="search-page"]')).toBe(true); 25 | }); 26 | 27 | test('can render index page', () => { 28 | const wrapper = shallow(); 29 | expect(wrapper.exists('[data-test="index-page"]')).toBe(true); 30 | }); 31 | 32 | test('renders search form', () => { 33 | const wrapper = shallow(); 34 | expect(wrapper.exists('[data-test="search-form"]')).toBe(true); 35 | }); 36 | 37 | test('renders actions', () => { 38 | const wrapper = shallow(); 39 | expect(wrapper.exists('[data-test="actions"]')).toBe(true); 40 | }); 41 | 42 | test('renders aside', () => { 43 | const wrapper = shallow(); 44 | expect(wrapper.exists('[data-test="aside"]')).toBe(true); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /src/components/search/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { connect } from 'react-redux'; 3 | import DetailPage from './pages/Detail/DetailPage'; 4 | import IndexPage from './pages/Index/IndexPage'; 5 | import SearchPage from './pages/Search/SearchPage'; 6 | import Actions from './partials/Actions/Actions'; 7 | import Aside from './partials/Aside/Aside'; 8 | import SearchForm from './partials/SearchForm/SearchForm'; 9 | import { RootState } from './store/reducer'; 10 | import { ViewState } from './store/slices/search/slices/view/state'; 11 | import { appLayout } from './styles'; 12 | 13 | const mapStateToProps = (state: RootState) => ({ 14 | view: state.search.view, 15 | }); 16 | 17 | export type AppReduxStateProps = ReturnType; 18 | 19 | export type AppProps = AppReduxStateProps; 20 | 21 | export class App extends React.Component { 22 | constructor(props: AppProps) { 23 | super(props); 24 | } 25 | 26 | renderPageComponent() { 27 | const { view } = this.props; 28 | 29 | switch (view) { 30 | case ViewState.Detail: 31 | return ; 32 | case ViewState.Search: 33 | return ; 34 | case ViewState.Index: 35 | return ; 36 | default: 37 | return; 38 | } 39 | } 40 | 41 | render() { 42 | return ( 43 |
44 | 45 | 46 |
49 | ); 50 | } 51 | } 52 | 53 | export default connect(mapStateToProps, {})(App); 54 | -------------------------------------------------------------------------------- /src/components/search/components/BookmarkList/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | 3 | const bookmarkListBtnWithNoSpacing = css` 4 | padding: 0; 5 | `; 6 | 7 | const bookmarkListContainer = css` 8 | display: flex; 9 | width: 100%; 10 | flex-wrap: wrap; 11 | justify-content: space-between; 12 | 13 | > * { 14 | flex: 1 1 100%; 15 | max-width: 100%; 16 | } 17 | 18 | @media screen and (max-width: 1024px) { 19 | > * { 20 | flex: 1 1 100%; 21 | margin-top: 8px; 22 | } 23 | } 24 | `; 25 | 26 | const bookmarkListContainerMultiCol = css` 27 | > * { 28 | flex: 1 1 calc(50% - 5px); 29 | max-width: calc(50% - 5px); 30 | } 31 | 32 | > *:nth-child(2n + 3), 33 | > *:nth-child(2n + 4) { 34 | margin-top: 8px; 35 | } 36 | `; 37 | 38 | export { bookmarkListBtnWithNoSpacing, bookmarkListContainer, bookmarkListContainerMultiCol }; 39 | -------------------------------------------------------------------------------- /src/components/search/components/Card/Card.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import React from 'react'; 3 | import { GrafanaThemeType } from '@grafana/data'; 4 | import { getTheme } from '@grafana/ui'; 5 | import { Card } from './Card'; 6 | 7 | describe('', () => { 8 | const theme = getTheme(GrafanaThemeType.Light); 9 | 10 | test('renders without crashing', () => { 11 | shallow( 12 | 13 |

Test

14 |
15 | ); 16 | }); 17 | 18 | test('should have default background "strong"', () => { 19 | const card = shallow( 20 | 21 |

Default "strong"

22 |
23 | ); 24 | expect(card.render().prop('data-test')).toBe('strong'); 25 | }); 26 | 27 | test('accepts both "strong" and "weak" background types', () => { 28 | shallow( 29 | 30 |

Strong Bg

31 |
32 | ); 33 | 34 | shallow( 35 | 36 |

Weak Bg

37 |
38 | ); 39 | }); 40 | 41 | test('renders passed children', () => { 42 | const child =
Test
; 43 | const card = shallow( 44 | 45 | {child} 46 | 47 | ); 48 | expect(card.find('[data-test="child"]').length).toBe(1); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/search/components/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Themeable, withTheme } from '@grafana/ui'; 3 | import { getCardStyles } from './styles'; 4 | 5 | export interface CardBasicProps { 6 | background?: 'weak' | 'strong'; 7 | children?: React.ReactNode; 8 | } 9 | 10 | export type CardProps = Themeable & CardBasicProps; 11 | 12 | export const Card: React.FC = (props: React.PropsWithChildren) => { 13 | const background = props.background ?? 'strong'; 14 | const styles = getCardStyles(props.theme, background); 15 | 16 | return ( 17 |
18 | {props.children} 19 |
20 | ); 21 | }; 22 | 23 | export default withTheme(Card); 24 | -------------------------------------------------------------------------------- /src/components/search/components/Card/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | import { GrafanaTheme } from '@grafana/data'; 3 | import { stylesFactory } from '@grafana/ui'; 4 | 5 | export const getCardStyles = stylesFactory((theme: GrafanaTheme, background: 'weak' | 'strong') => { 6 | return { 7 | container: css` 8 | width: 100%; 9 | padding: ${theme.spacing.md}; 10 | border-radius: ${theme.border.radius.sm}; 11 | background: ${background === 'strong' ? theme.colors.bg2 : theme.colors.bg1}; 12 | `, 13 | }; 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/search/components/Loader/Loader.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, shallow } from 'enzyme'; 2 | import React from 'react'; 3 | import { GrafanaThemeType } from '@grafana/data'; 4 | import { getTheme } from '@grafana/ui'; 5 | import { Loader } from './Loader'; 6 | 7 | describe('', () => { 8 | const theme = getTheme(GrafanaThemeType.Light); 9 | 10 | test('renders without crashing', () => { 11 | shallow(); 12 | }); 13 | 14 | test('renders loading indicator and children when loaded = false', () => { 15 | const component = render(); 16 | expect(component.find('[data-test="spinner-container"]').length).toBe(1); 17 | expect(component.find('[data-test="content-container"]').length).toBe(1); 18 | }); 19 | 20 | test('renders children only when loaded = true', () => { 21 | const childNode = 'Cheerio'; 22 | const component = render( 23 | 24 | {childNode} 25 | 26 | ); 27 | expect(component.html()).toBe(childNode); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /src/components/search/components/Loader/Loader.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from 'emotion'; 2 | import React from 'react'; 3 | import { Spinner, Themeable, withTheme } from '@grafana/ui'; 4 | import { spinner, spinnerContainer, spinnerOuter } from './styles'; 5 | 6 | export interface LoaderBasicProps { 7 | loaded: boolean; 8 | boundedContainer?: boolean; 9 | children?: React.ReactNode; 10 | } 11 | 12 | export type LoaderProps = Themeable & LoaderBasicProps; 13 | 14 | export class Loader extends React.Component { 15 | constructor(props: LoaderProps) { 16 | super(props); 17 | } 18 | 19 | render() { 20 | const { loaded, theme } = this.props; 21 | if (loaded) { 22 | return this.props.children; 23 | } 24 | return ( 25 |
26 |
27 |
36 | 37 |
38 |
{this.props.children}
39 |
40 |
41 | ); 42 | } 43 | } 44 | 45 | export default withTheme(Loader); 46 | -------------------------------------------------------------------------------- /src/components/search/components/Loader/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | 3 | const spinnerOuter = css` 4 | height: 200px; 5 | max-height: 100%; 6 | `; 7 | 8 | const spinnerContainer = css` 9 | position: relative; 10 | height: 100%; 11 | `; 12 | 13 | const spinner = css` 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | bottom: 0; 18 | right: 0; 19 | display: flex; 20 | width: 100%; 21 | height: 100%; 22 | justify-content: center; 23 | align-items: center; 24 | z-index: 20; 25 | `; 26 | 27 | export { spinnerOuter, spinnerContainer, spinner }; 28 | -------------------------------------------------------------------------------- /src/components/search/components/SearchHistoryList/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | 3 | const searchHistoryListBtnWithNoSpacing = css` 4 | padding: 0; 5 | `; 6 | 7 | const searchHistoryListContainer = css` 8 | display: flex; 9 | width: 100%; 10 | flex-wrap: wrap; 11 | justify-content: space-between; 12 | 13 | > * { 14 | flex: 1 1 100%; 15 | max-width: 100%; 16 | } 17 | 18 | @media screen and (max-width: 1024px) { 19 | > * { 20 | flex: 1 1 100%; 21 | margin-top: 8px; 22 | } 23 | } 24 | `; 25 | 26 | const searchHistoryListContainerMultiCol = css` 27 | > * { 28 | flex: 1 1 calc(50% - 5px); 29 | max-width: calc(50% - 5px); 30 | } 31 | 32 | > *:nth-child(2n + 3), 33 | > *:nth-child(2n + 4) { 34 | margin-top: 8px; 35 | } 36 | `; 37 | 38 | export { searchHistoryListBtnWithNoSpacing, searchHistoryListContainer, searchHistoryListContainerMultiCol }; 39 | -------------------------------------------------------------------------------- /src/components/search/components/SearchResult/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | import { GrafanaTheme } from '@grafana/data'; 3 | 4 | const searchResultItem = css` 5 | width: 100%; 6 | display: flex; 7 | flex-direction: column; 8 | `; 9 | 10 | const searchResultHeader = css``; 11 | 12 | const searchResultTitle = css` 13 | margin-bottom: 16px; 14 | `; 15 | 16 | const searchResultDescription = css` 17 | margin-bottom: 8px; 18 | white-space: pre-line; 19 | 20 | > p:last-child { 21 | margin-bottom: 0; 22 | } 23 | `; 24 | 25 | const searchResultFooter = css``; 26 | 27 | const searchResultBtnWithNoSpacing = css` 28 | padding: 0; 29 | `; 30 | 31 | const searchResultEntityType = (theme: GrafanaTheme) => css` 32 | padding: 0; 33 | cursor: default; 34 | pointer-events: none; 35 | text-transform: capitalize; 36 | color: ${theme.colors.text}; 37 | `; 38 | 39 | const searchResultTitleLink = (theme: GrafanaTheme) => css` 40 | padding: 0; 41 | color: ${theme.colors.text}; 42 | font-size: ${theme.typography.heading.h4}; 43 | font-weight: normal; 44 | 45 | &:hover { 46 | color: ${theme.colors.linkExternal}; 47 | } 48 | `; 49 | 50 | export { 51 | searchResultItem, 52 | searchResultHeader, 53 | searchResultTitle, 54 | searchResultDescription, 55 | searchResultFooter, 56 | searchResultBtnWithNoSpacing, 57 | searchResultEntityType, 58 | searchResultTitleLink, 59 | }; 60 | -------------------------------------------------------------------------------- /src/components/search/components/withServices/withServices.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ServicesContext from '../../contexts/services'; 3 | import { Services } from '../../services/services'; 4 | 5 | export interface WithServicesProps { 6 | services: Services; 7 | } 8 | 9 | const withServices =

(Component: React.ComponentType

) => { 10 | return function WithServices(props: Pick>) { 11 | // https://github.com/Microsoft/TypeScript/issues/28938 12 | // since TS 3.2 spread erases type 13 | return ( 14 | 15 | {services => } 16 | 17 | ); 18 | }; 19 | }; 20 | 21 | export default withServices; 22 | -------------------------------------------------------------------------------- /src/components/search/config/config.ts: -------------------------------------------------------------------------------- 1 | const Config = Object.freeze({ 2 | // Maximum search shortcuts available from index 3 | MAX_SEARCH_SHORTCUTS: 12, 4 | REQUEST_TIMEOUT: 2500, 5 | RESULTS_PER_PAGE: 8, 6 | ALLOW_SEARCH_SUGGESTIONS: true, 7 | }); 8 | 9 | export default Config; 10 | -------------------------------------------------------------------------------- /src/components/search/contexts/services.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Services } from '../services/services'; 3 | 4 | // Ugly hack -> we never use do ServiceContext.Provider without populating context with value first. 5 | const ServicesContext = React.createContext({} as Services); 6 | 7 | export default ServicesContext; 8 | -------------------------------------------------------------------------------- /src/components/search/mocks/endpoints.ts: -------------------------------------------------------------------------------- 1 | import { AutocompleteResponse, SearchEntity, TextResponse } from '../../../common/services/pmsearch/types'; 2 | import { PmApiIndomEndpointResponse, PmApiMetricMetricResponse } from '../models/endpoints/pmapi'; 3 | import { detailEntities, indomEntities, searchEntities } from './responses'; 4 | 5 | export const querySearchEndpoint = ( 6 | pattern: string, 7 | entityFlags: SearchEntity, 8 | limit: number, 9 | offset: number 10 | ): Promise => { 11 | return new Promise((resolve, reject) => { 12 | setTimeout(() => { 13 | resolve({ 14 | elapsed: 0, 15 | results: searchEntities.slice(0, limit), 16 | limit, 17 | offset, 18 | total: 25, 19 | }); 20 | }, 1000); 21 | }); 22 | }; 23 | 24 | // For now, lets assume this always finds the entity and the entity is always metric name 25 | export const metricFetchEndpoint = (metricId: string): Promise => { 26 | return new Promise((resolve, reject) => { 27 | setTimeout(() => { 28 | resolve(detailEntities.find(x => x.metrics.some(m => m.name === metricId))!.metrics[0]); 29 | }, 1000); 30 | }); 31 | }; 32 | 33 | // For testing autocomplete 34 | export const autocompleteFetchEndpoint = (query: string): Promise => { 35 | return new Promise((resolve, reject) => { 36 | setTimeout(() => { 37 | resolve(['metric.name1', 'metrika2', 'metrický metr', 'bazooka', 'extraordinary', 'zlatý důl']); 38 | }, 100); 39 | }); 40 | }; 41 | 42 | // Separate endpoint, will be fetched lazily 43 | export const indomFetchEndpoint = (indom: string): Promise => { 44 | return new Promise((resolve, reject) => { 45 | setTimeout(() => { 46 | resolve(indomEntities.find(x => x.indom === indom)!); 47 | }, 1000); 48 | }); 49 | }; 50 | -------------------------------------------------------------------------------- /src/components/search/models/endpoints/pmapi.ts: -------------------------------------------------------------------------------- 1 | export interface PmApiLabelsResponse { 2 | [key: string]: string; 3 | } 4 | 5 | export interface PmApiMetricMetricResponse { 6 | name: string; 7 | series: string; 8 | pmid: string; 9 | type: string; 10 | indom?: string; 11 | sem: string; 12 | units: string; 13 | labels: PmApiLabelsResponse; 14 | 'text-oneline': string; 15 | 'text-help': string; 16 | } 17 | 18 | export interface PmApiMetricEndpointResponse { 19 | context: number; 20 | metrics: PmApiMetricMetricResponse[]; 21 | } 22 | 23 | export interface PmApiIndomEndpointInstanceResponse { 24 | instance: number; 25 | name: string; 26 | labels: PmApiLabelsResponse; 27 | } 28 | 29 | export interface PmApiIndomEndpointResponse { 30 | context: number; 31 | indom: string; 32 | labels: PmApiLabelsResponse; 33 | 'text-oneline': string; 34 | 'text-help': string; 35 | instances: PmApiIndomEndpointInstanceResponse[]; 36 | } 37 | -------------------------------------------------------------------------------- /src/components/search/models/entities/indom.ts: -------------------------------------------------------------------------------- 1 | import { TextItemResponse } from '../../../../common/services/pmsearch/types'; 2 | 3 | export type IndomEntitySparseItem = Omit; 4 | 5 | export interface IndomEntity { 6 | indom: IndomEntitySparseItem; 7 | metrics: IndomEntitySparseItem[]; 8 | instances: IndomEntitySparseItem[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/components/search/models/entities/metric.ts: -------------------------------------------------------------------------------- 1 | export interface MetricEntityMeta { 2 | indom: string; 3 | pmid: string; 4 | semantics: string; 5 | type: string; 6 | units: string; 7 | source: string; 8 | } 9 | 10 | export interface MetricEntityLabels { 11 | [key: string]: string | number | boolean; 12 | } 13 | 14 | export interface MetricEntitySeries { 15 | series: string; 16 | meta: MetricEntityMeta; 17 | labels?: MetricEntityLabels; 18 | } 19 | 20 | export type MetricSiblingsEntity = string[]; 21 | 22 | export interface MetricEntity { 23 | name: string; 24 | // These are monkey patched for now 25 | oneline?: string; 26 | help?: string; 27 | series: MetricEntitySeries[]; 28 | } 29 | -------------------------------------------------------------------------------- /src/components/search/models/errors/errors.ts: -------------------------------------------------------------------------------- 1 | export class PluginInitError extends Error { 2 | constructor(message?: string) { 3 | super(message); 4 | this.name = 'PluginInitError'; 5 | Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/components/search/pages/Detail/InstanceDomain/Instances/Instances.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import React from 'react'; 3 | import { Instances, InstancesProps } from './Instances'; 4 | 5 | describe('Instance Domain ', () => { 6 | let instancesProps: InstancesProps; 7 | 8 | beforeEach(() => { 9 | instancesProps = { 10 | instances: [ 11 | { 12 | name: 'virbr0-nic', 13 | }, 14 | { 15 | name: 'virbr0', 16 | }, 17 | { 18 | name: 'wlp0s20f3', 19 | }, 20 | { 21 | name: 'ens20u2', 22 | }, 23 | { 24 | name: 'lo', 25 | }, 26 | { 27 | name: 'veth2d4d8bb', 28 | }, 29 | { 30 | name: 'docker0', 31 | }, 32 | ], 33 | }; 34 | }); 35 | 36 | test('renders without crashing', () => { 37 | shallow(); 38 | }); 39 | 40 | test('displays all instances provided', () => { 41 | const wrapper = shallow(); 42 | instancesProps.instances.forEach(instance => { 43 | expect(wrapper.exists(`[data-test="instance-${instance.name}"]`)).toBe(true); 44 | }); 45 | }); 46 | 47 | test('handles no instances case', () => { 48 | shallow(); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /src/components/search/pages/Detail/InstanceDomain/Instances/Instances.tsx: -------------------------------------------------------------------------------- 1 | import { cx } from 'emotion'; 2 | import React from 'react'; 3 | import { VerticalGroup } from '@grafana/ui'; 4 | import { IndomEntitySparseItem } from '../../../../models/entities/indom'; 5 | import { gridItem, gridList, gridListSingleCol, gridValue } from '../../styles'; 6 | 7 | export interface InstancesProps { 8 | instances: IndomEntitySparseItem[]; 9 | } 10 | 11 | export class Instances extends React.Component { 12 | render() { 13 | const { instances } = this.props; 14 | 15 | if (instances.length) { 16 | return ( 17 | 18 |

Instances:

19 |
20 | {instances.map((instance, i) => ( 21 |
22 | 23 | {instance.name} 24 | 25 |
26 | ))} 27 |
28 | 29 | ); 30 | } 31 | 32 | return

No instances.

; 33 | } 34 | } 35 | 36 | export default Instances; 37 | -------------------------------------------------------------------------------- /src/components/search/pages/Detail/Metric/Labels/Labels.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import React from 'react'; 3 | import { Labels, LabelsProps } from './Labels'; 4 | 5 | describe('Metric ', () => { 6 | let labelsProps: LabelsProps; 7 | 8 | beforeEach(() => { 9 | labelsProps = { 10 | labels: { 11 | test0: 'string1', 12 | test1: '', 13 | test2: 1337, 14 | test3: 3.14, 15 | test4: true, 16 | test5: false, 17 | }, 18 | }; 19 | }); 20 | 21 | test('renders without crashing', () => { 22 | shallow(); 23 | }); 24 | 25 | test('displays arbitrary labels', () => { 26 | const wrapper = shallow(); 27 | Object.entries(labelsProps.labels).forEach(([key, value]) => { 28 | expect(wrapper.exists(`[data-test="${key}"]`)).toBe(true); 29 | expect(wrapper.find(`[data-test="${key}-value"]`).text()).toBe(value.toString()); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/components/search/pages/Detail/Metric/Labels/Labels.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { MetricEntityLabels } from '../../../../models/entities/metric'; 3 | import { gridItem, gridList, gridTitle, gridValue } from '../../styles'; 4 | 5 | export interface LabelsProps { 6 | labels: MetricEntityLabels; 7 | } 8 | 9 | export class Labels extends React.Component { 10 | render() { 11 | const { labels } = this.props; 12 | return ( 13 |
14 | {Object.entries(labels).map(([key, value], i) => ( 15 |
16 | {key}: 17 | 18 | {value.toString()} 19 | 20 |
21 | ))} 22 |
23 | ); 24 | } 25 | } 26 | 27 | export default Labels; 28 | -------------------------------------------------------------------------------- /src/components/search/pages/Detail/Metric/Meta/Meta.test.tsx: -------------------------------------------------------------------------------- 1 | import { shallow } from 'enzyme'; 2 | import React from 'react'; 3 | import { Meta, MetaProps } from './Meta'; 4 | 5 | describe('Metric ', () => { 6 | let metaProps: MetaProps; 7 | 8 | beforeEach(() => { 9 | metaProps = { 10 | onIndomClick: jest.fn(), 11 | meta: { 12 | pmid: 'pmid', 13 | type: 'type', 14 | semantics: 'semantics', 15 | units: 'units', 16 | indom: 'indom', 17 | source: 'source', 18 | }, 19 | }; 20 | }); 21 | 22 | test('renders without crashing', () => { 23 | shallow(); 24 | }); 25 | 26 | test('displays pmid', () => { 27 | const wrapper = shallow(); 28 | expect(wrapper.exists('[data-test="pmid"]')).toBe(true); 29 | }); 30 | 31 | test('displays type', () => { 32 | const wrapper = shallow(); 33 | expect(wrapper.exists('[data-test="type"]')).toBe(true); 34 | }); 35 | 36 | test('displays semantics', () => { 37 | const wrapper = shallow(); 38 | expect(wrapper.exists('[data-test="semantics"]')).toBe(true); 39 | }); 40 | 41 | test('displays units', () => { 42 | const wrapper = shallow(); 43 | expect(wrapper.exists('[data-test="units"]')).toBe(true); 44 | }); 45 | 46 | test('displays indom', () => { 47 | const wrapper = shallow(); 48 | expect(wrapper.exists('[data-test="indom"]')).toBe(true); 49 | }); 50 | 51 | test('displays source', () => { 52 | const wrapper = shallow(); 53 | expect(wrapper.exists('[data-test="source"]')).toBe(true); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/components/search/pages/Detail/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | import { GrafanaTheme } from '@grafana/data'; 3 | 4 | const detailPageContainer = css` 5 | grid-area: content; 6 | `; 7 | 8 | const detailPageItem = css` 9 | width: 100%; 10 | display: flex; 11 | flex-direction: column; 12 | `; 13 | 14 | const detailPageHeader = css``; 15 | 16 | const detailPageTitle = css` 17 | margin-bottom: 16px; 18 | `; 19 | 20 | const detailPageEntityType = (theme: GrafanaTheme) => css` 21 | margin-bottom: 16px; 22 | padding: 0; 23 | cursor: default; 24 | pointer-events: none; 25 | text-transform: capitalize; 26 | color: ${theme.colors.text}; 27 | `; 28 | 29 | const detailPageDescription = css` 30 | margin-bottom: 8px; 31 | white-space: pre-line; 32 | 33 | > p:last-child { 34 | margin-bottom: 0; 35 | } 36 | `; 37 | 38 | const detailPageActions = css` 39 | margin-top: 16px; 40 | `; 41 | 42 | const detailPageProperties = css` 43 | width: 100%; 44 | `; 45 | 46 | const detailPageBtn = css` 47 | padding: 0; 48 | `; 49 | 50 | const gridList = css` 51 | display: flex; 52 | width: 100%; 53 | flex-wrap: wrap; 54 | justify-content: space-between; 55 | 56 | > * { 57 | flex: 1 1 calc(50% - 5px); 58 | max-width: calc(50% - 5px); 59 | } 60 | 61 | > *:nth-child(2n + 3), 62 | > *:nth-child(2n + 4) { 63 | margin-top: 8px; 64 | } 65 | 66 | @media screen and (max-width: 600px) { 67 | > * { 68 | flex: 1 1 100%; 69 | margin-top: 8px; 70 | } 71 | } 72 | `; 73 | 74 | const gridListSingleCol = css` 75 | > * { 76 | flex: 1 1 100%; 77 | max-width: 100%; 78 | } 79 | 80 | > * + * { 81 | margin-top: 8px; 82 | } 83 | `; 84 | 85 | const gridItem = css``; 86 | 87 | const gridTitle = css` 88 | display: block; 89 | font-weight: bold; 90 | `; 91 | 92 | const gridValue = css` 93 | display: block; 94 | `; 95 | 96 | const instanceDomainContent = css``; 97 | 98 | const instanceDomainItemList = css` 99 | margin-left: 1rem; 100 | margin-bottom: 1rem; 101 | `; 102 | 103 | const radioBtnGroupContainer = css` 104 | width: 100%; 105 | `; 106 | 107 | export { 108 | detailPageContainer, 109 | detailPageItem, 110 | detailPageHeader, 111 | detailPageTitle, 112 | detailPageEntityType, 113 | detailPageDescription, 114 | detailPageActions, 115 | detailPageProperties, 116 | detailPageBtn, 117 | gridList, 118 | gridListSingleCol, 119 | gridItem, 120 | gridTitle, 121 | gridValue, 122 | radioBtnGroupContainer, 123 | instanceDomainContent, 124 | instanceDomainItemList, 125 | }; 126 | -------------------------------------------------------------------------------- /src/components/search/pages/Index/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | 3 | const indexPageContainer = css` 4 | grid-area: content; 5 | `; 6 | 7 | const indexPageBtnWithNoSpacing = css` 8 | padding: 0; 9 | `; 10 | 11 | const indexColumnsList = css` 12 | display: flex; 13 | width: 100%; 14 | flex-wrap: wrap; 15 | justify-content: space-between; 16 | 17 | > * { 18 | flex: 1 1 50%; 19 | } 20 | 21 | > *:nth-child(2n + 3), 22 | > *:nth-child(2n + 4) { 23 | margin-top: 8px; 24 | } 25 | 26 | @media screen and (max-width: 1024px) { 27 | > * { 28 | flex: 1 1 100%; 29 | margin-top: 8px; 30 | } 31 | } 32 | `; 33 | 34 | export { indexPageContainer, indexPageBtnWithNoSpacing, indexColumnsList }; 35 | -------------------------------------------------------------------------------- /src/components/search/pages/Search/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | import { GrafanaTheme } from '@grafana/data'; 3 | 4 | const searchPageContainer = css` 5 | grid-area: content; 6 | `; 7 | 8 | const searchPageMatchesDesc = (theme: GrafanaTheme) => css` 9 | display: inline-block; 10 | color: ${theme.colors.textSemiWeak}; 11 | `; 12 | 13 | const searchPageElapsed = (theme: GrafanaTheme) => css` 14 | display: inline-block; 15 | font-style: italic; 16 | color: ${theme.colors.textWeak}; 17 | `; 18 | 19 | const paginationContainer = css` 20 | margin: 0 auto; 21 | `; 22 | 23 | export { searchPageContainer, searchPageMatchesDesc, searchPageElapsed, paginationContainer }; 24 | -------------------------------------------------------------------------------- /src/components/search/partials/Actions/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | 3 | const actionsContainer = css` 4 | grid-area: actions; 5 | `; 6 | 7 | const actionsBtnWithNoSpacing = css` 8 | padding: 0; 9 | `; 10 | 11 | export { actionsContainer, actionsBtnWithNoSpacing }; 12 | -------------------------------------------------------------------------------- /src/components/search/partials/Aside/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | import { GrafanaTheme } from '@grafana/data'; 3 | 4 | const asideContainer = css` 5 | grid-area: aside; 6 | `; 7 | 8 | const asideButton = css` 9 | padding-left: 0; 10 | padding-right: 0; 11 | `; 12 | 13 | const asideButtonInactive = (theme: GrafanaTheme) => css` 14 | color: ${theme.colors.text}; 15 | cursor: default; 16 | 17 | &:hover { 18 | color: ${theme.colors.text}; 19 | } 20 | `; 21 | 22 | export { asideContainer, asideButton, asideButtonInactive }; 23 | -------------------------------------------------------------------------------- /src/components/search/services/services.ts: -------------------------------------------------------------------------------- 1 | import { getBackendSrv } from '@grafana/runtime'; 2 | import { PmSearchApiService } from '../../../common/services/pmsearch/PmSearchApiService'; 3 | import { PmSeriesApiService } from '../../../common/services/pmseries/PmSeriesApiService'; 4 | import { GenericError } from '../../../common/types/errors'; 5 | import valkeyPluginConfig from '../../../datasources/valkey/plugin.json'; 6 | import Config from '../config/config'; 7 | import EntityService from './EntityDetailService'; 8 | 9 | export interface Services { 10 | searchService: PmSearchApiService; 11 | seriesService: PmSeriesApiService; 12 | entityService: EntityService; 13 | } 14 | 15 | async function getDatasourceSettings() { 16 | const datasources = await getBackendSrv().get('/api/datasources'); 17 | const valkeyDatasource = datasources.find((ds: any) => ds.type === valkeyPluginConfig.id); 18 | if (!valkeyDatasource) { 19 | throw new GenericError( 20 | `Could not find any PCP Valkey data source. Please create a PCP Valkey data source before using the search feature.` 21 | ); 22 | } 23 | return valkeyDatasource; 24 | } 25 | 26 | export const initServices = async (): Promise => { 27 | const settings = await getDatasourceSettings(); 28 | const searchService = new PmSearchApiService(getBackendSrv(), { 29 | dsInstanceSettings: settings, 30 | baseUrl: settings.url!, 31 | timeoutMs: Config.REQUEST_TIMEOUT, 32 | }); 33 | const seriesService = new PmSeriesApiService(getBackendSrv(), { 34 | dsInstanceSettings: settings, 35 | baseUrl: settings.url!, 36 | timeoutMs: Config.REQUEST_TIMEOUT, 37 | }); 38 | const entityService = new EntityService(searchService, seriesService); 39 | return { searchService, seriesService, entityService }; 40 | }; 41 | -------------------------------------------------------------------------------- /src/components/search/store/reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { searchReducer } from './slices/search/reducer'; 3 | 4 | const rootReducer = combineReducers({ 5 | search: searchReducer, 6 | }); 7 | 8 | export type RootState = ReturnType; 9 | 10 | export default rootReducer; 11 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/reducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import { persistReducer } from 'redux-persist'; 3 | import storage from 'redux-persist/lib/storage'; 4 | import { bookmarksReducer } from './slices/bookmarks/reducer'; 5 | import { entityReducer } from './slices/entity/reducer'; 6 | import { historyReducer } from './slices/history/reducer'; 7 | import { queryReducer } from './slices/query/reducer'; 8 | import { resultReducer } from './slices/result/reducer'; 9 | import { viewReducer } from './slices/view/reducer'; 10 | 11 | const persistanceConfig = { 12 | key: 'grafana-pcp-app:search', 13 | storage: storage, 14 | whitelist: ['bookmarks', 'history'], 15 | }; 16 | 17 | const searchReducer = persistReducer( 18 | persistanceConfig, 19 | combineReducers({ 20 | bookmarks: bookmarksReducer, 21 | view: viewReducer, 22 | entity: entityReducer, 23 | query: queryReducer, 24 | history: historyReducer, 25 | result: resultReducer, 26 | }) 27 | ); 28 | 29 | export { searchReducer }; 30 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/shared/state.ts: -------------------------------------------------------------------------------- 1 | import { SearchEntity } from '../../../../../../common/services/pmsearch/types'; 2 | 3 | export enum FetchStatus { 4 | INIT, 5 | PENDING, 6 | SUCCESS, 7 | ERROR, 8 | } 9 | 10 | export interface SearchQuery { 11 | pattern: string; 12 | entityFlags: SearchEntity; 13 | pageNum: number; 14 | } 15 | 16 | export interface TrackableStatus { 17 | status: FetchStatus; 18 | } 19 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/bookmarks/actionCreators.ts: -------------------------------------------------------------------------------- 1 | import { AddBookmarkAction, ClearBookmarksAction, RemoveBookmarkAction } from './actions'; 2 | import { BookmarkItem } from './state'; 3 | import { ADD_BOOKMARK, CLEAR_BOOKMARKS, REMOVE_BOOKMARK } from './types'; 4 | 5 | export const addBookmark = (item: BookmarkItem): AddBookmarkAction => { 6 | return { 7 | type: ADD_BOOKMARK, 8 | payload: item, 9 | }; 10 | }; 11 | 12 | export const removeBookmark = (item: BookmarkItem): RemoveBookmarkAction => { 13 | return { 14 | type: REMOVE_BOOKMARK, 15 | payload: item, 16 | }; 17 | }; 18 | 19 | export type ClearBookmarksActionCreator = () => ClearBookmarksAction; 20 | 21 | export const clearBookmarks: ClearBookmarksActionCreator = () => { 22 | return { type: CLEAR_BOOKMARKS }; 23 | }; 24 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/bookmarks/actions.ts: -------------------------------------------------------------------------------- 1 | import { BookmarkItem } from './state'; 2 | import { ADD_BOOKMARK, CLEAR_BOOKMARKS, REMOVE_BOOKMARK } from './types'; 3 | 4 | export interface AddBookmarkAction { 5 | type: typeof ADD_BOOKMARK; 6 | payload: BookmarkItem; 7 | } 8 | 9 | export interface RemoveBookmarkAction { 10 | type: typeof REMOVE_BOOKMARK; 11 | payload: BookmarkItem; 12 | } 13 | 14 | export interface ClearBookmarksAction { 15 | type: typeof CLEAR_BOOKMARKS; 16 | } 17 | 18 | export type BookmarksAction = ClearBookmarksAction | AddBookmarkAction | RemoveBookmarkAction; 19 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/bookmarks/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux'; 2 | import { BookmarksAction } from './actions'; 3 | import { BookmarksState, initialBookmarks } from './state'; 4 | import { ADD_BOOKMARK, CLEAR_BOOKMARKS, REMOVE_BOOKMARK } from './types'; 5 | 6 | const bookmarksReducer: Reducer = (state, action) => { 7 | if (state === undefined) { 8 | return initialBookmarks(); 9 | } 10 | switch (action.type) { 11 | case ADD_BOOKMARK: 12 | return [action.payload, ...state]; 13 | case REMOVE_BOOKMARK: 14 | return state.filter(x => x.id !== action.payload.id && x.type !== action.payload.type); 15 | case CLEAR_BOOKMARKS: 16 | return []; 17 | default: 18 | return state; 19 | } 20 | }; 21 | 22 | export { bookmarksReducer }; 23 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/bookmarks/state.ts: -------------------------------------------------------------------------------- 1 | import { EntityType } from '../../../../../../../common/services/pmsearch/types'; 2 | 3 | export interface BookmarkItem { 4 | // Is also human readable name 5 | id: string; 6 | type: EntityType; 7 | } 8 | 9 | export type BookmarksState = BookmarkItem[]; 10 | 11 | export const initialBookmarks = (): BookmarksState => []; 12 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/bookmarks/types.ts: -------------------------------------------------------------------------------- 1 | export const ADD_BOOKMARK = 'ADD_BOOKMARK'; 2 | export const REMOVE_BOOKMARK = 'REMOVE_BOOKMARK'; 3 | export const CLEAR_BOOKMARKS = 'CLEAR_BOOKMARKS'; 4 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/entity/actions.ts: -------------------------------------------------------------------------------- 1 | import { IndomData, MetricData, MetricSiblingsData } from './state'; 2 | import { 3 | LOAD_INDOM_ERROR, 4 | LOAD_INDOM_INIT, 5 | LOAD_INDOM_PENDING, 6 | LOAD_INDOM_SUCCESS, 7 | LOAD_METRIC_ERROR, 8 | LOAD_METRIC_INIT, 9 | LOAD_METRIC_PENDING, 10 | LOAD_METRIC_SIBLINGS_ERROR, 11 | LOAD_METRIC_SIBLINGS_INIT, 12 | LOAD_METRIC_SIBLINGS_PENDING, 13 | LOAD_METRIC_SIBLINGS_SUCCESS, 14 | LOAD_METRIC_SUCCESS, 15 | } from './types'; 16 | 17 | export interface LoadMetricInitAction { 18 | type: typeof LOAD_METRIC_INIT; 19 | } 20 | 21 | export interface LoadMetricPendingAction { 22 | type: typeof LOAD_METRIC_PENDING; 23 | } 24 | 25 | export interface LoadMetricSuccessAction { 26 | type: typeof LOAD_METRIC_SUCCESS; 27 | payload: MetricData; 28 | } 29 | 30 | export interface LoadMetricErrorAction { 31 | type: typeof LOAD_METRIC_ERROR; 32 | } 33 | 34 | export type LoadMetricAction = 35 | | LoadMetricInitAction 36 | | LoadMetricPendingAction 37 | | LoadMetricSuccessAction 38 | | LoadMetricErrorAction; 39 | 40 | export interface LoadMetricSiblingsInitAction { 41 | type: typeof LOAD_METRIC_SIBLINGS_INIT; 42 | } 43 | 44 | export interface LoadMetricSiblingsPendingAction { 45 | type: typeof LOAD_METRIC_SIBLINGS_PENDING; 46 | } 47 | 48 | export interface LoadMetricSiblingsSuccessAction { 49 | type: typeof LOAD_METRIC_SIBLINGS_SUCCESS; 50 | payload: MetricSiblingsData; 51 | } 52 | 53 | export interface LoadMetricSiblingsErrorAction { 54 | type: typeof LOAD_METRIC_SIBLINGS_ERROR; 55 | } 56 | 57 | export type LoadMetricSiblingsAction = 58 | | LoadMetricSiblingsInitAction 59 | | LoadMetricSiblingsPendingAction 60 | | LoadMetricSiblingsSuccessAction 61 | | LoadMetricSiblingsErrorAction; 62 | 63 | export interface LoadIndomInitAction { 64 | type: typeof LOAD_INDOM_INIT; 65 | } 66 | 67 | export interface LoadIndomPendingAction { 68 | type: typeof LOAD_INDOM_PENDING; 69 | } 70 | 71 | export interface LoadIndomSuccessAction { 72 | type: typeof LOAD_INDOM_SUCCESS; 73 | payload: IndomData; 74 | } 75 | 76 | export interface LoadIndomErrorAction { 77 | type: typeof LOAD_INDOM_ERROR; 78 | } 79 | 80 | export type LoadIndomAction = 81 | | LoadIndomInitAction 82 | | LoadIndomPendingAction 83 | | LoadIndomSuccessAction 84 | | LoadIndomErrorAction; 85 | 86 | export type EntityAction = LoadMetricAction | LoadMetricSiblingsAction | LoadIndomAction; 87 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/entity/state.ts: -------------------------------------------------------------------------------- 1 | import { EntityType } from '../../../../../../../common/services/pmsearch/types'; 2 | import { IndomEntity } from '../../../../../models/entities/indom'; 3 | import { MetricEntity, MetricSiblingsEntity } from '../../../../../models/entities/metric'; 4 | import { TrackableStatus } from '../../shared/state'; 5 | 6 | export interface MetricData { 7 | data: MetricEntity | null; 8 | } 9 | 10 | export interface MetricSiblingsData { 11 | data: MetricSiblingsEntity | null; 12 | } 13 | 14 | export interface IndomData { 15 | data: IndomEntity | null; 16 | } 17 | 18 | export type MetricDataState = MetricData & TrackableStatus; 19 | 20 | export type MetricSiblingsDataState = MetricSiblingsData & TrackableStatus; 21 | 22 | export type IndomDataState = IndomData & TrackableStatus; 23 | 24 | export interface MetricDetailState { 25 | type: EntityType.Metric; 26 | metric: MetricDataState; 27 | siblings?: MetricSiblingsDataState; 28 | } 29 | 30 | export interface InstanceDomainDetailState { 31 | type: EntityType.InstanceDomain; 32 | indom: IndomDataState; 33 | } 34 | 35 | export type EntityState = MetricDetailState | InstanceDomainDetailState | null; 36 | 37 | export const initialEntity = (): EntityState => null; 38 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/entity/types.ts: -------------------------------------------------------------------------------- 1 | export const LOAD_METRIC_INIT = 'LOAD_METRIC_INIT'; 2 | export const LOAD_METRIC_PENDING = 'LOAD_METRIC_PENDING'; 3 | export const LOAD_METRIC_SUCCESS = 'LOAD_METRIC_SUCCESS'; 4 | export const LOAD_METRIC_ERROR = 'LOAD_METRIC_ERROR'; 5 | export const LOAD_METRIC_SIBLINGS_INIT = 'LOAD_METRIC_SIBLINGS_INIT'; 6 | export const LOAD_METRIC_SIBLINGS_PENDING = 'LOAD_METRIC_SIBLINGS_PENDING'; 7 | export const LOAD_METRIC_SIBLINGS_SUCCESS = 'LOAD_METRIC_SIBLINGS_SUCCESS'; 8 | export const LOAD_METRIC_SIBLINGS_ERROR = 'LOAD_METRIC_SIBLINGS_ERROR'; 9 | export const LOAD_INDOM_INIT = 'LOAD_INDOM_INIT'; 10 | export const LOAD_INDOM_PENDING = 'LOAD_INDOM_PENDING'; 11 | export const LOAD_INDOM_SUCCESS = 'LOAD_INDOM_SUCCESS'; 12 | export const LOAD_INDOM_ERROR = 'LOAD_INDOM_ERROR'; 13 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/history/actionCreators.ts: -------------------------------------------------------------------------------- 1 | import { ClearHistoryAction } from './actions'; 2 | import { CLEAR_HISTORY } from './types'; 3 | 4 | export type ClearSearchHistoryActionCreator = () => ClearHistoryAction; 5 | 6 | export const clearSearchHistory: ClearSearchHistoryActionCreator = () => { 7 | return { type: CLEAR_HISTORY }; 8 | }; 9 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/history/actions.ts: -------------------------------------------------------------------------------- 1 | import { SearchQuery } from '../../shared/state'; 2 | import { ADD_HISTORY, CLEAR_HISTORY } from './types'; 3 | 4 | export interface AddHistoryAction { 5 | type: typeof ADD_HISTORY; 6 | payload: SearchQuery; 7 | } 8 | 9 | export interface ClearHistoryAction { 10 | type: typeof CLEAR_HISTORY; 11 | } 12 | 13 | export type HistoryAction = ClearHistoryAction | AddHistoryAction; 14 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/history/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux'; 2 | import Config from '../../../../../config/config'; 3 | import { HistoryAction } from './actions'; 4 | import { HistoryState, initialHistory } from './state'; 5 | import { ADD_HISTORY, CLEAR_HISTORY } from './types'; 6 | 7 | const historyReducer: Reducer = (state, action) => { 8 | if (state === undefined) { 9 | return initialHistory(); 10 | } 11 | switch (action.type) { 12 | case ADD_HISTORY: { 13 | const newState = [action.payload, ...state]; 14 | if (newState.length > Config.MAX_SEARCH_SHORTCUTS) { 15 | newState.pop(); 16 | } 17 | return newState; 18 | } 19 | case CLEAR_HISTORY: 20 | return []; 21 | default: 22 | return state; 23 | } 24 | }; 25 | 26 | export { historyReducer }; 27 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/history/state.ts: -------------------------------------------------------------------------------- 1 | import { SearchQuery } from '../../shared/state'; 2 | 3 | export type HistoryState = SearchQuery[]; 4 | 5 | export const initialHistory = (): HistoryState => []; 6 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/history/types.ts: -------------------------------------------------------------------------------- 1 | export const ADD_HISTORY = 'ADD_HISTORY'; 2 | export const CLEAR_HISTORY = 'CLEAR_HISTORY'; 3 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/query/actions.ts: -------------------------------------------------------------------------------- 1 | import { SearchQuery } from '../../shared/state'; 2 | import { CLEAR_QUERY, SET_QUERY } from './types'; 3 | 4 | export interface ClearQueryAction { 5 | type: typeof CLEAR_QUERY; 6 | } 7 | 8 | export interface SetQueryAction { 9 | type: typeof SET_QUERY; 10 | payload: SearchQuery; 11 | } 12 | 13 | export type QueryAction = ClearQueryAction | SetQueryAction; 14 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/query/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux'; 2 | import { QueryAction } from './actions'; 3 | import { initialQuery, initialState, QueryState } from './state'; 4 | import { CLEAR_QUERY, SET_QUERY } from './types'; 5 | 6 | const queryReducer: Reducer = (state, action) => { 7 | if (state === undefined) { 8 | return initialState; 9 | } 10 | switch (action.type) { 11 | case SET_QUERY: 12 | return action.payload; 13 | case CLEAR_QUERY: 14 | return initialQuery(); 15 | default: 16 | return state; 17 | } 18 | }; 19 | 20 | export { queryReducer }; 21 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/query/state.ts: -------------------------------------------------------------------------------- 1 | import { SearchEntity } from '../../../../../../../common/services/pmsearch/types'; 2 | import { SearchQuery } from '../../shared/state'; 3 | 4 | export type QueryState = SearchQuery; 5 | 6 | export const initialQuery = (): SearchQuery => ({ 7 | pattern: '', 8 | entityFlags: SearchEntity.All, 9 | pageNum: 1, 10 | }); 11 | 12 | export const initialState: SearchQuery = initialQuery(); 13 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/query/types.ts: -------------------------------------------------------------------------------- 1 | export const SET_QUERY = 'SET_QUERY'; 2 | export const CLEAR_QUERY = 'CLEAR_QUERY'; 3 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/result/actions.ts: -------------------------------------------------------------------------------- 1 | import { ResultData } from './state'; 2 | import { LOAD_RESULT_ERROR, LOAD_RESULT_INIT, LOAD_RESULT_PENDING, LOAD_RESULT_SUCCESS } from './types'; 3 | 4 | export interface LoadResultInitAction { 5 | type: typeof LOAD_RESULT_INIT; 6 | } 7 | 8 | export interface LoadResultPendingAction { 9 | type: typeof LOAD_RESULT_PENDING; 10 | } 11 | 12 | export interface LoadResultSuccessAction { 13 | type: typeof LOAD_RESULT_SUCCESS; 14 | payload: ResultData; 15 | } 16 | 17 | export interface LoadResultErrorAction { 18 | type: typeof LOAD_RESULT_ERROR; 19 | error: any; 20 | } 21 | 22 | export type LoadResultAction = 23 | | LoadResultInitAction 24 | | LoadResultPendingAction 25 | | LoadResultSuccessAction 26 | | LoadResultErrorAction; 27 | 28 | export type ResultAction = LoadResultAction; 29 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/result/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux'; 2 | import { FetchStatus } from '../../shared/state'; 3 | import { ResultAction } from './actions'; 4 | import { initialState, ResultState } from './state'; 5 | import { LOAD_RESULT_ERROR, LOAD_RESULT_INIT, LOAD_RESULT_PENDING, LOAD_RESULT_SUCCESS } from './types'; 6 | 7 | const resultReducer: Reducer = (state, action) => { 8 | if (state === undefined) { 9 | return initialState; 10 | } 11 | switch (action.type) { 12 | case LOAD_RESULT_INIT: 13 | // Preserve old results while new results started fetching 14 | return { 15 | ...(state ?? { data: null }), 16 | status: FetchStatus.INIT, 17 | }; 18 | case LOAD_RESULT_PENDING: 19 | if (state) { 20 | return { 21 | ...state, 22 | status: FetchStatus.PENDING, 23 | }; 24 | } 25 | break; 26 | case LOAD_RESULT_SUCCESS: 27 | if (state) { 28 | return { 29 | status: FetchStatus.SUCCESS, 30 | data: action.payload.data, 31 | }; 32 | } 33 | break; 34 | case LOAD_RESULT_ERROR: 35 | if (state) { 36 | return { 37 | status: FetchStatus.ERROR, 38 | data: null, 39 | error: action.error, 40 | }; 41 | } 42 | break; 43 | default: 44 | return state; 45 | } 46 | return state; 47 | }; 48 | 49 | export { resultReducer }; 50 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/result/state.ts: -------------------------------------------------------------------------------- 1 | import { TextResponse } from '../../../../../../../common/services/pmsearch/types'; 2 | import { FetchStatus, TrackableStatus } from '../../shared/state'; 3 | 4 | export interface ResultData { 5 | data: TextResponse | null; 6 | error?: any; 7 | } 8 | 9 | export type ResultDataState = ResultData & TrackableStatus; 10 | 11 | export type ResultState = ResultDataState; 12 | 13 | export const initialState: ResultState = { status: FetchStatus.INIT, data: null }; 14 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/result/types.ts: -------------------------------------------------------------------------------- 1 | export const LOAD_RESULT_INIT = 'LOAD_RESULT_INIT'; 2 | export const LOAD_RESULT_PENDING = 'LOAD_RESULT_PENDING'; 3 | export const LOAD_RESULT_SUCCESS = 'LOAD_RESULT_SUCCESS'; 4 | export const LOAD_RESULT_ERROR = 'LOAD_RESULT_ERROR'; 5 | export const CLEAR_RESULTS = 'CLEAR_RESULTS'; 6 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/view/actionCreators.ts: -------------------------------------------------------------------------------- 1 | import { SetViewAction } from './actions'; 2 | import { ViewState } from './state'; 3 | import { SET_VIEW } from './types'; 4 | 5 | export type SetViewActionCreator = (view: ViewState) => SetViewAction; 6 | 7 | export const setView: SetViewActionCreator = view => { 8 | return { type: SET_VIEW, payload: view }; 9 | }; 10 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/view/actions.ts: -------------------------------------------------------------------------------- 1 | import { ViewState } from './state'; 2 | import { SET_VIEW } from './types'; 3 | 4 | export interface SetViewAction { 5 | type: typeof SET_VIEW; 6 | payload: ViewState; 7 | } 8 | 9 | export type ViewAction = SetViewAction; 10 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/view/reducer.ts: -------------------------------------------------------------------------------- 1 | import { Reducer } from 'redux'; 2 | import { ViewAction } from './actions'; 3 | import { initialView, ViewState } from './state'; 4 | import { SET_VIEW } from './types'; 5 | 6 | const viewReducer: Reducer = (state, action) => { 7 | if (state === undefined) { 8 | return initialView(); 9 | } 10 | switch (action.type) { 11 | case SET_VIEW: 12 | return action.payload; 13 | default: 14 | return state; 15 | } 16 | }; 17 | 18 | export { viewReducer }; 19 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/view/state.ts: -------------------------------------------------------------------------------- 1 | export enum ViewState { 2 | Detail, 3 | Search, 4 | Index, 5 | } 6 | 7 | export const initialView = () => ViewState.Index; 8 | -------------------------------------------------------------------------------- /src/components/search/store/slices/search/slices/view/types.ts: -------------------------------------------------------------------------------- 1 | export const SET_VIEW = 'SET_VIEW'; 2 | -------------------------------------------------------------------------------- /src/components/search/store/store.ts: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, compose, createStore } from 'redux'; 2 | import { persistStore } from 'redux-persist'; 3 | import thunk from 'redux-thunk'; 4 | import { Services } from '../services/services'; 5 | import rootReducer from './reducer'; 6 | 7 | const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; 8 | 9 | const initStore = async (services: Services) => { 10 | const middleware = thunk.withExtraArgument(services); 11 | const store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(middleware))); 12 | const persistor = persistStore(store); 13 | return { store, persistor }; 14 | }; 15 | 16 | export { initStore }; 17 | -------------------------------------------------------------------------------- /src/components/search/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | 3 | const appLayout = css` 4 | display: grid; 5 | grid-template-areas: 6 | 'header actions' 7 | 'content aside'; 8 | grid-template-columns: auto 250px; 9 | grid-template-rows: auto auto; 10 | grid-gap: 32px; 11 | 12 | @media screen and (max-width: 1024px) { 13 | grid-template-areas: 'header' 'content' 'actions' 'aside'; 14 | grid-template-columns: 1fr; 15 | grid-template-rows: auto auto; 16 | } 17 | `; 18 | 19 | const wrappedBtn = css` 20 | > span { 21 | min-width: 0; 22 | } 23 | 24 | > span > span:nth-child(2) { 25 | white-space: nowrap; 26 | overflow: hidden; 27 | text-overflow: ellipsis; 28 | } 29 | `; 30 | export { appLayout, wrappedBtn }; 31 | -------------------------------------------------------------------------------- /src/components/search/utils/SearchEntityUtil.ts: -------------------------------------------------------------------------------- 1 | import { EntityType, SearchEntity } from '../../../common/services/pmsearch/types'; 2 | 3 | export class SearchEntityUtil { 4 | static toEntityTypes(searchEntity: SearchEntity): EntityType[] { 5 | const result: EntityType[] = []; 6 | if (searchEntity & SearchEntity.Metrics) { 7 | result.push(EntityType.Metric); 8 | } 9 | if (searchEntity & SearchEntity.Instances) { 10 | result.push(EntityType.Instance); 11 | } 12 | if (searchEntity & SearchEntity.InstanceDomains) { 13 | result.push(EntityType.InstanceDomain); 14 | } 15 | return result; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/components/search/utils/utils.ts: -------------------------------------------------------------------------------- 1 | // Use only when you trust HTML source! 2 | const stripHtml = (html: string) => { 3 | const el = document.createElement('div'); 4 | el.innerHTML = html; 5 | return el.textContent || el.innerText || ''; 6 | }; 7 | 8 | export { stripHtml }; 9 | -------------------------------------------------------------------------------- /src/dashboards/valkey/preview/pcp-valkey-metric-preview-graph.jsonnet: -------------------------------------------------------------------------------- 1 | local grafana = import 'grafonnet/grafana.libsonnet'; 2 | 3 | grafana.dashboard.new( 4 | 'PCP Valkey: Metric Preview (Graph)', // Gets converted to slug URL => /d/pcp-valkey-metric-preview-graph/pcp-valkey-metric-preview-graph 5 | uid='pcp-valkey-metric-preview-graph', 6 | tags=['pcp-valkey'], 7 | time_from='now-6h', 8 | time_to='now', 9 | refresh='10s', 10 | ) 11 | .addTemplate( 12 | grafana.template.datasource( 13 | 'datasource', 14 | 'performancecopilot-valkey-datasource', 15 | 'PCP Valkey', 16 | hide='variable', 17 | ) 18 | ) 19 | .addTemplate( 20 | grafana.template.new( 21 | 'metric', 22 | '$datasource', 23 | 'metrics()', 24 | multi=true, 25 | sort=1, // asc 26 | refresh='load', 27 | ) 28 | ) 29 | .addPanel( 30 | grafana.graphPanel 31 | .new( 32 | '$metric', 33 | datasource='$datasource', 34 | repeat='metric', 35 | ) 36 | .addTarget({ 37 | expr: '$metric', 38 | format: 'time_series', 39 | }), 40 | gridPos={ 41 | x: 0, 42 | y: 0, 43 | w: 24, 44 | h: 20, 45 | } 46 | ) + { 47 | revision: 3, 48 | } 49 | -------------------------------------------------------------------------------- /src/dashboards/valkey/preview/pcp-valkey-metric-preview-table.jsonnet: -------------------------------------------------------------------------------- 1 | local grafana = import 'grafonnet/grafana.libsonnet'; 2 | 3 | grafana.dashboard.new( 4 | 'PCP Valkey: Metric Preview (Table)', // Gets converted to slug URL => /d/pcp-valkey-metric-preview-table/pcp-valkey-metric-preview-table 5 | uid='pcp-valkey-metric-preview-table', 6 | tags=['pcp-valkey'], 7 | time_from='now-6h', 8 | time_to='now', 9 | refresh='10s', 10 | ) 11 | .addTemplate( 12 | grafana.template.datasource( 13 | 'datasource', 14 | 'performancecopilot-valkey-datasource', 15 | 'PCP Valkey', 16 | hide='variable', 17 | ) 18 | ) 19 | .addTemplate( 20 | grafana.template.new( 21 | 'metric', 22 | '$datasource', 23 | 'metrics()', 24 | multi=true, 25 | sort=1, // asc 26 | refresh='load', 27 | ) 28 | ) 29 | .addPanel( 30 | grafana.tablePanel 31 | .new( 32 | '$metric', 33 | datasource='$datasource', 34 | styles=null, 35 | ) 36 | .addTarget({ 37 | expr: '$metric', 38 | format: 'time_series', 39 | }) + { 40 | repeat: 'metric', 41 | }, 42 | gridPos={ 43 | x: 0, 44 | y: 0, 45 | w: 24, 46 | h: 20, 47 | } 48 | ) + { 49 | revision: 3, 50 | } 51 | -------------------------------------------------------------------------------- /src/dashboards/vector/checklist/_breadcrumbspanel.libsonnet: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | * Returns a new breadcrumbs panel that can be added in a row 4 | * 5 | * @name breadcrumbsPanel.new 6 | * 7 | * @param title The title of the panel, preferably empty, so it stays transparent 8 | * @param items List of lists of tree nodes from 'nodes' field, located in DASHBOARD_DIR/overview/shared.libsonnet (preferably retrieved by *getNavigation* or *getNodeByUid* function from same file), representing dashboards. Each list represents single level of depth within the tree of navigation items that will be rendered. When multiple items are present in a depth, select UI is used, when single item is present, link is used. 9 | * 10 | * @func addItem chainable declarative addition of *items* 11 | * @func addItems 12 | * 13 | * --- 14 | * Is not intended for use in user's dashboards 15 | */ 16 | new( 17 | title='', 18 | items=[], 19 | ):: { 20 | title: title, 21 | type: 'performancecopilot-breadcrumbs-panel', 22 | options: { 23 | items: items, 24 | }, 25 | addItem(item):: self { 26 | options+: { 27 | items+: [item], 28 | }, 29 | }, 30 | addItems(items):: std.foldl(function(b, item) b.addItem(item), items, self), 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /src/dashboards/vector/checklist/pcp-vector-checklist-cpu-sys.jsonnet: -------------------------------------------------------------------------------- 1 | local breadcrumbsPanel = import '_breadcrumbspanel.libsonnet'; 2 | local troubleshootingPanel = import '_troubleshootingpanel.libsonnet'; 3 | local grafana = import 'grafonnet/grafana.libsonnet'; 4 | 5 | local checklist = import 'checklist.libsonnet'; 6 | local node = checklist.getNodeByUid('pcp-vector-checklist-cpu-sys'); 7 | local parents = checklist.getParentNodes(node); 8 | 9 | checklist.dashboard.new(node) 10 | .addPanel( 11 | troubleshootingPanel.panel.new( 12 | title='Intensive tasks in kernel-space', 13 | datasource='$datasource', 14 | unit='percentunit', 15 | troubleshooting=troubleshootingPanel.troubleshooting.new( 16 | name='CPU - Intensive tasks in kernel-space', 17 | metrics=[ 18 | troubleshootingPanel.metric.new( 19 | 'hotproc.psinfo.stime', 20 | 'time (in ms) spent executing system code (calls) since process started', 21 | ), 22 | ], 23 | urls=['https://access.redhat.com/articles/781993'], 24 | notes="To enable metric collection for this panel, please configure the hotproc.control.config setting. It can be set with: sudo pmstore hotproc.control.config 'cpuburn > 0.05'", 25 | parents=parents, 26 | ), 27 | ).addTargets([ 28 | { expr: 'hotproc.psinfo.stime', format: 'time_series', legendFormat: '$instance', url: '$url', hostspec: '$hostspec' }, 29 | ]), gridPos={ 30 | x: 0, 31 | y: 3, 32 | w: 12, 33 | h: 9, 34 | }, 35 | ) + { 36 | revision: 3, 37 | } 38 | -------------------------------------------------------------------------------- /src/dashboards/vector/checklist/pcp-vector-checklist-cpu-user.jsonnet: -------------------------------------------------------------------------------- 1 | local breadcrumbsPanel = import '_breadcrumbspanel.libsonnet'; 2 | local troubleshootingPanel = import '_troubleshootingpanel.libsonnet'; 3 | local grafana = import 'grafonnet/grafana.libsonnet'; 4 | 5 | local checklist = import 'checklist.libsonnet'; 6 | local node = checklist.getNodeByUid('pcp-vector-checklist-cpu-user'); 7 | local parents = checklist.getParentNodes(node); 8 | 9 | checklist.dashboard.new(node) 10 | .addPanel( 11 | troubleshootingPanel.panel.new( 12 | title='Intensive tasks in user-space', 13 | datasource='$datasource', 14 | unit='percentunit', 15 | troubleshooting=troubleshootingPanel.troubleshooting.new( 16 | name='CPU - Intensive tasks in user-space', 17 | metrics=[ 18 | troubleshootingPanel.metric.new( 19 | 'hotproc.psinfo.utime', 20 | 'time (in ms) spent executing user code since process started', 21 | ), 22 | ], 23 | urls=['https://access.redhat.com/articles/781993'], 24 | notes="To enable metric collection for this panel, please configure the hotproc.control.config setting. It can be set with: sudo pmstore hotproc.control.config 'cpuburn > 0.05'", 25 | parents=parents, 26 | ), 27 | ).addTargets([ 28 | { expr: 'hotproc.psinfo.utime', format: 'time_series', legendFormat: '$instance', url: '$url', hostspec: '$hostspec' }, 29 | ]), gridPos={ 30 | x: 0, 31 | y: 3, 32 | w: 12, 33 | h: 9, 34 | }, 35 | ) + { 36 | revision: 3, 37 | } 38 | -------------------------------------------------------------------------------- /src/dashboards/vector/checklist/pcp-vector-checklist-memory-swap.jsonnet: -------------------------------------------------------------------------------- 1 | local breadcrumbsPanel = import '_breadcrumbspanel.libsonnet'; 2 | local troubleshootingPanel = import '_troubleshootingpanel.libsonnet'; 3 | local grafana = import 'grafonnet/grafana.libsonnet'; 4 | 5 | local checklist = import 'checklist.libsonnet'; 6 | local node = checklist.getNodeByUid('pcp-vector-checklist-memory-swap'); 7 | local parents = checklist.getParentNodes(node); 8 | 9 | checklist.dashboard.new(node) 10 | .addPanel( 11 | troubleshootingPanel.panel.new( 12 | title='Available system memory', 13 | datasource='$datasource', 14 | unit='percentunit', 15 | troubleshooting=troubleshootingPanel.troubleshooting.new( 16 | name='Memory - Low system memory', 17 | metrics=[ 18 | troubleshootingPanel.metric.new( 19 | 'mem.util.free', 20 | 'free memory metric from /proc/meminfo', 21 | ), 22 | troubleshootingPanel.metric.new( 23 | 'mem.physmem', 24 | 'total system memory metric reported by /proc/meminfo', 25 | ), 26 | ], 27 | derivedMetrics=[ 28 | troubleshootingPanel.derivedMetric.new( 29 | 'mem.ratio.free', 30 | 'mem.util.free / mem.physmem' 31 | ), 32 | ], 33 | urls=['https://access.redhat.com/solutions/406253'], 34 | parents=parents, 35 | ), 36 | ).addTargets([ 37 | { expr: 'mem.util.free / mem.physmem', format: 'time_series', legendFormat: '$expr', url: '$url', hostspec: '$hostspec' }, 38 | ]), gridPos={ 39 | x: 0, 40 | y: 3, 41 | w: 12, 42 | h: 9, 43 | }, 44 | ) 45 | .addPanel( 46 | troubleshootingPanel.panel.new( 47 | title='Available NUMA node memory', 48 | datasource='$datasource', 49 | unit='percentunit', 50 | troubleshooting=troubleshootingPanel.troubleshooting.new( 51 | name='Memory - Low NUMA node memory', 52 | metrics=[ 53 | troubleshootingPanel.metric.new( 54 | 'mem.numa.util.free', 55 | 'per-node free memory', 56 | ), 57 | troubleshootingPanel.metric.new( 58 | 'mem.numa.util.total', 59 | 'per-node total memory', 60 | ), 61 | ], 62 | derivedMetrics=[ 63 | troubleshootingPanel.derivedMetric.new( 64 | 'mem.numa.ratio.free', 65 | 'mem.numa.util.free / mem.numa.util.total' 66 | ), 67 | ], 68 | urls=['https://access.redhat.com/solutions/465463'], 69 | parents=parents, 70 | ), 71 | ).addTargets([ 72 | { expr: 'mem.numa.util.free / mem.numa.util.total', format: 'time_series', legendFormat: '$instance', url: '$url', hostspec: '$hostspec' }, 73 | ]), gridPos={ 74 | x: 12, 75 | y: 3, 76 | w: 12, 77 | h: 9, 78 | }, 79 | ) + { 80 | revision: 3, 81 | } 82 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/README.md: -------------------------------------------------------------------------------- 1 | # PCP bpftrace 2 | 3 | #### [Data source documentation](https://grafana-pcp.readthedocs.io/en/latest/datasources/bpftrace.html) 4 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/config.ts: -------------------------------------------------------------------------------- 1 | export const Config = { 2 | /** default refresh interval if not specified in the URL query params */ 3 | defaultRefreshIntervalMs: 1000, 4 | 5 | /** set timeout to 6s, because pmproxy timeout is 5s (e.g. connecting to a remote pmcd) */ 6 | apiTimeoutMs: 6000, 7 | 8 | /** 9 | * don't remove targets immediately if not requested in refreshInterval 10 | * also instruct pmproxy to not clear the context immediately if not used in refreshInterval 11 | */ 12 | gracePeriodMs: 10000, 13 | 14 | defaults: { 15 | hostspec: 'pcp://127.0.0.1', 16 | retentionTime: '30m', 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/dashboards/pcp-bpftrace-flame-graphs.jsonnet: -------------------------------------------------------------------------------- 1 | local grafana = import 'grafonnet/grafana.libsonnet'; 2 | 3 | local flameGraph( 4 | title, 5 | datasource=null, 6 | ) = { 7 | title: title, 8 | type: 'performancecopilot-flamegraph-panel', 9 | datasource: datasource, 10 | 11 | _nextTarget:: 0, 12 | addTarget(target):: self { 13 | local nextTarget = super._nextTarget, 14 | _nextTarget: nextTarget + 1, 15 | targets+: [target { refId: std.char(std.codepoint('A') + nextTarget) }], 16 | }, 17 | addTargets(targets):: std.foldl(function(p, t) p.addTarget(t), targets, self), 18 | }; 19 | 20 | grafana.dashboard.new( 21 | 'PCP bpftrace: Flame Graphs', 22 | tags=['pcp-bpftrace', 'eBPF'], 23 | time_from='now-5s', 24 | time_to='now', 25 | refresh='1s', 26 | timepicker=grafana.timepicker.new( 27 | refresh_intervals=['1s', '2s', '5s', '10s'], 28 | ), 29 | ) 30 | .addTemplate( 31 | grafana.template.datasource( 32 | 'bpftrace_datasource', 33 | 'performancecopilot-bpftrace-datasource', 34 | 'PCP bpftrace', 35 | ) 36 | ) 37 | .addTemplate( 38 | grafana.template.datasource( 39 | 'vector_datasource', 40 | 'performancecopilot-vector-datasource', 41 | 'PCP Vector', 42 | ) 43 | ) 44 | .addPanel( 45 | grafana.text.new( 46 | 'Installation Instructions', 47 | mode='markdown', 48 | content='This dashboards requires the [bpftrace PMDA](https://man7.org/linux/man-pages/man1/pmdabpftrace.1.html) to be installed and configured with *dynamic_scripts* enabled.', 49 | ), gridPos={ 50 | x: 0, 51 | y: 0, 52 | w: 24, 53 | h: 2, 54 | } 55 | ) 56 | .addPanel( 57 | grafana.graphPanel.new( 58 | 'CPU Utilization', 59 | datasource='$vector_datasource', 60 | format='percent', 61 | min=0, 62 | stack=true, 63 | legend_show=false, 64 | time_from='5m', 65 | ) 66 | .addTargets([ 67 | { expr: 'kernel.cpu.util.user', format: 'time_series' }, 68 | { expr: 'kernel.cpu.util.sys', format: 'time_series' }, 69 | ]), gridPos={ 70 | x: 0, 71 | y: 2, 72 | w: 24, 73 | h: 4, 74 | } 75 | ) 76 | .addPanel( 77 | flameGraph( 78 | 'Kernel Stacks', 79 | datasource='$bpftrace_datasource', 80 | ) 81 | .addTargets([ 82 | { expr: importstr 'tools/kstacks.bt', format: 'flamegraph' }, 83 | ]), gridPos={ 84 | x: 0, 85 | y: 6, 86 | w: 24, 87 | h: 8, 88 | } 89 | ) 90 | .addPanel( 91 | flameGraph( 92 | 'User Stacks', 93 | datasource='$bpftrace_datasource', 94 | ) 95 | .addTargets([ 96 | { expr: importstr 'tools/ustacks.bt', format: 'flamegraph' }, 97 | ]), gridPos={ 98 | x: 0, 99 | y: 14, 100 | w: 24, 101 | h: 8, 102 | } 103 | ) + { 104 | revision: 3, 105 | } 106 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/dashboards/tools/biolatency.bt: -------------------------------------------------------------------------------- 1 | /* 2 | * biolatency.bt Block I/O latency as a histogram. 3 | * For Linux, uses bpftrace, eBPF. 4 | * 5 | * This is a bpftrace version of the bcc tool of the same name. 6 | * 7 | * Copyright 2018 Netflix, Inc. 8 | * Licensed under the Apache License, Version 2.0 (the "License") 9 | * 10 | * 13-Sep-2018 Brendan Gregg Created this. 11 | */ 12 | // include: @usecs 13 | 14 | kprobe:blk_account_io_start 15 | { 16 | @start[arg0] = nsecs; 17 | } 18 | 19 | kprobe:blk_account_io_done 20 | /@start[arg0]/ 21 | { 22 | @usecs = hist((nsecs - @start[arg0]) / 1000); 23 | delete(@start[arg0]); 24 | } 25 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/dashboards/tools/cpuwalk.bt: -------------------------------------------------------------------------------- 1 | /* 2 | * cpuwalk Sample which CPUs are executing processes. 3 | * For Linux, uses bpftrace and eBPF. 4 | * 5 | * USAGE: cpuwalk.bt 6 | * 7 | * This is a bpftrace version of the DTraceToolkit tool of the same name. 8 | * 9 | * Copyright 2018 Netflix, Inc. 10 | * Licensed under the Apache License, Version 2.0 (the "License") 11 | * 12 | * 08-Sep-2018 Brendan Gregg Created this. 13 | */ 14 | 15 | profile:hz:99 16 | /pid/ 17 | { 18 | @cpu = lhist(cpu, 0, 1000, 1); 19 | } 20 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/dashboards/tools/kstacks.bt: -------------------------------------------------------------------------------- 1 | /* 2 | * sample kernel stacks every 99 Hz, and clear map every 5 seconds 3 | * 4 | * 30-Oct-2019 Andreas Gerstmayr Created this. 5 | * 7-Aug-2020 Andreas Gerstmayr Added process name and PID. 6 | */ 7 | // include: @stacks 8 | // custom-output-block 9 | 10 | profile:hz:99 { @stacks[comm,pid,kstack] = count(); } 11 | 12 | interval:s:1 { 13 | print(@stacks); 14 | @cnt++; 15 | if (@cnt >= 5) { 16 | clear(@stacks); 17 | @cnt = 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/dashboards/tools/runqlat.bt: -------------------------------------------------------------------------------- 1 | /* 2 | * runqlat.bt CPU scheduler run queue latency as a histogram. 3 | * For Linux, uses bpftrace, eBPF. 4 | * 5 | * This is a bpftrace version of the bcc tool of the same name. 6 | * 7 | * Copyright 2018 Netflix, Inc. 8 | * Licensed under the Apache License, Version 2.0 (the "License") 9 | * 10 | * 17-Sep-2018 Brendan Gregg Created this. 11 | */ 12 | // include: @usecs 13 | 14 | #include 15 | 16 | tracepoint:sched:sched_wakeup, 17 | tracepoint:sched:sched_wakeup_new 18 | { 19 | @qtime[args->pid] = nsecs; 20 | } 21 | 22 | tracepoint:sched:sched_switch 23 | { 24 | if (args->prev_state == TASK_RUNNING) { 25 | @qtime[args->prev_pid] = nsecs; 26 | } 27 | 28 | $ns = @qtime[args->next_pid]; 29 | if ($ns) { 30 | @usecs = hist((nsecs - $ns) / 1000); 31 | } 32 | delete(@qtime[args->next_pid]); 33 | } 34 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/dashboards/tools/runqlen.bt: -------------------------------------------------------------------------------- 1 | /* 2 | * runqlen.bt CPU scheduler run queue length as a histogram. 3 | * For Linux, uses bpftrace, eBPF. 4 | * 5 | * This is a bpftrace version of the bcc tool of the same name. 6 | * 7 | * Copyright 2018 Netflix, Inc. 8 | * Licensed under the Apache License, Version 2.0 (the "License") 9 | * 10 | * 07-Oct-2018 Brendan Gregg Created this. 11 | */ 12 | 13 | #include 14 | 15 | // Until BTF is available, we'll need to declare some of this struct manually, 16 | // since it isn't avaible to be #included. This will need maintenance to match 17 | // your kernel version. It is from kernel/sched/sched.h: 18 | struct cfs_rq_partial { 19 | struct load_weight load; 20 | unsigned long runnable_weight; 21 | unsigned int nr_running; 22 | unsigned int h_nr_running; 23 | }; 24 | 25 | profile:hz:99 26 | { 27 | $task = (struct task_struct *)curtask; 28 | $my_q = (struct cfs_rq_partial *)$task->se.cfs_rq; 29 | $len = $my_q->nr_running; 30 | $len = $len > 0 ? $len - 1 : 0; // subtract currently runing task 31 | @runqlen = lhist($len, 0, 100, 1); 32 | } 33 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/dashboards/tools/syscall_count.bt: -------------------------------------------------------------------------------- 1 | tracepoint:syscalls:sys_enter_open, tracepoint:syscalls:sys_enter_openat { @open = count(); } 2 | tracepoint:syscalls:sys_enter_read { @read = count(); } 3 | tracepoint:syscalls:sys_enter_write { @write = count(); } 4 | tracepoint:syscalls:sys_enter_recvmsg { @recvmsg = count(); } 5 | tracepoint:syscalls:sys_enter_sendmsg { @sendmsg = count(); } 6 | tracepoint:syscalls:sys_enter_execve, tracepoint:syscalls:sys_enter_execveat { @execve = count(); } 7 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/dashboards/tools/tcpaccept.bt: -------------------------------------------------------------------------------- 1 | /* 2 | * tcpaccept.bt Trace TCP accept()s 3 | * For Linux, uses bpftrace and eBPF. 4 | * 5 | * USAGE: tcpaccept.bt 6 | * 7 | * This is a bpftrace version of the bcc tool of the same name. 8 | * 9 | * This uses dynamic tracing of the kernel inet_csk_accept() socket function 10 | * (from tcp_prot.accept), and will need to be modified to match kernel changes. 11 | 12 | * Copyright (c) 2018 Dale Hamel. 13 | * Licensed under the Apache License, Version 2.0 (the "License") 14 | 15 | * 23-Nov-2018 Dale Hamel created this. 16 | * 23-Aug-2019 Andreas Gerstmayr added CSV output 17 | */ 18 | // table-retain-lines: 10 19 | 20 | #include 21 | #include 22 | 23 | BEGIN 24 | { 25 | printf("%s,%s,%s,", "TIME", "PID", "COMM"); 26 | printf("%s,%s,%s,%s,%s\n", "RADDR", "RPORT", "LADDR", 27 | "LPORT", "BL"); 28 | } 29 | 30 | kretprobe:inet_csk_accept 31 | { 32 | $sk = (struct sock *)retval; 33 | $inet_family = $sk->__sk_common.skc_family; 34 | 35 | if ($inet_family == AF_INET || $inet_family == AF_INET6) { 36 | // initialize variable type: 37 | $daddr = ntop(0); 38 | $saddr = ntop(0); 39 | if ($inet_family == AF_INET) { 40 | $daddr = ntop($sk->__sk_common.skc_daddr); 41 | $saddr = ntop($sk->__sk_common.skc_rcv_saddr); 42 | } else { 43 | $daddr = ntop( 44 | $sk->__sk_common.skc_v6_daddr.in6_u.u6_addr8); 45 | $saddr = ntop( 46 | $sk->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr8); 47 | } 48 | $lport = $sk->__sk_common.skc_num; 49 | $dport = $sk->__sk_common.skc_dport; 50 | $qlen = $sk->sk_ack_backlog; 51 | $qmax = $sk->sk_max_ack_backlog; 52 | 53 | // Destination port is big endian, it must be flipped 54 | $dport = ($dport >> 8) | (($dport << 8) & 0x00FF00); 55 | 56 | time("%H:%M:%S,"); 57 | printf("%d,%s,", pid, comm); 58 | printf("%s,%d,%s,%d,", $daddr, $dport, $saddr, 59 | $lport); 60 | printf("%d/%d\n", $qlen, $qmax); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/dashboards/tools/tcpconnect.bt: -------------------------------------------------------------------------------- 1 | /* 2 | * tcpconnect.bt Trace TCP connect()s. 3 | * For Linux, uses bpftrace and eBPF. 4 | * 5 | * USAGE: tcpconnect.bt 6 | * 7 | * This is a bpftrace version of the bcc tool of the same name. 8 | * It is limited to ipv4 addresses. 9 | * 10 | * All connection attempts are traced, even if they ultimately fail. 11 | * 12 | * This uses dynamic tracing of kernel functions, and will need to be updated 13 | * to match kernel changes. 14 | * 15 | * Copyright (c) 2018 Dale Hamel. 16 | * Licensed under the Apache License, Version 2.0 (the "License") 17 | * 18 | * 23-Nov-2018 Dale Hamel created this. 19 | * 23-Aug-2019 Andreas Gerstmayr added CSV output 20 | */ 21 | // table-retain-lines: 10 22 | 23 | #include 24 | #include 25 | 26 | BEGIN 27 | { 28 | printf("%s,%s,%s,", "TIME", "PID", "COMM"); 29 | printf("%s,%s,%s,%s\n", "SADDR", "SPORT", "DADDR", "DPORT"); 30 | } 31 | 32 | kprobe:tcp_connect 33 | { 34 | $sk = ((struct sock *) arg0); 35 | $inet_family = $sk->__sk_common.skc_family; 36 | 37 | if ($inet_family == AF_INET || $inet_family == AF_INET6) { 38 | if ($inet_family == AF_INET) { 39 | $daddr = ntop($sk->__sk_common.skc_daddr); 40 | $saddr = ntop($sk->__sk_common.skc_rcv_saddr); 41 | } else { 42 | $daddr = ntop($sk->__sk_common.skc_v6_daddr.in6_u.u6_addr8); 43 | $saddr = ntop($sk->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr8); 44 | } 45 | $lport = $sk->__sk_common.skc_num; 46 | $dport = $sk->__sk_common.skc_dport; 47 | 48 | // Destination port is big endian, it must be flipped 49 | $dport = ($dport >> 8) | (($dport << 8) & 0x00FF00); 50 | 51 | time("%H:%M:%S,"); 52 | printf("%d,%s,", pid, comm); 53 | printf("%s,%d,%s,%d\n", $saddr, $lport, $daddr, $dport); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/dashboards/tools/tcpdrop.bt: -------------------------------------------------------------------------------- 1 | /* 2 | * tcpdrop.bt Trace TCP kernel-dropped packets/segments. 3 | * For Linux, uses bpftrace and eBPF. 4 | * 5 | * USAGE: tcpdrop.bt 6 | * 7 | * This is a bpftrace version of the bcc tool of the same name. 8 | * It is limited to ipv4 addresses, and cannot show tcp flags. 9 | * 10 | * This provides information such as packet details, socket state, and kernel 11 | * stack trace for packets/segments that were dropped via tcp_drop(). 12 | 13 | * Copyright (c) 2018 Dale Hamel. 14 | * Licensed under the Apache License, Version 2.0 (the "License") 15 | 16 | * 23-Nov-2018 Dale Hamel created this. 17 | * 23-Aug-2019 Andreas Gerstmayr added CSV output 18 | */ 19 | // include: @output 20 | // table-retain-lines: 10 21 | 22 | #include 23 | #include 24 | 25 | BEGIN 26 | { 27 | printf("%s,%s,%s,%s,%s,%s\n", "TIME", "PID", "COMM", "SADDR:SPORT", "DADDR:DPORT", "STATE"); 28 | 29 | // See https://github.com/torvalds/linux/blob/master/include/net/tcp_states.h 30 | @tcp_states[1] = "ESTABLISHED"; 31 | @tcp_states[2] = "SYN_SENT"; 32 | @tcp_states[3] = "SYN_RECV"; 33 | @tcp_states[4] = "FIN_WAIT1"; 34 | @tcp_states[5] = "FIN_WAIT2"; 35 | @tcp_states[6] = "TIME_WAIT"; 36 | @tcp_states[7] = "CLOSE"; 37 | @tcp_states[8] = "CLOSE_WAIT"; 38 | @tcp_states[9] = "LAST_ACK"; 39 | @tcp_states[10] = "LISTEN"; 40 | @tcp_states[11] = "CLOSING"; 41 | @tcp_states[12] = "NEW_SYN_RECV"; 42 | } 43 | 44 | kprobe:tcp_drop 45 | { 46 | $sk = ((struct sock *) arg0); 47 | $inet_family = $sk->__sk_common.skc_family; 48 | 49 | if ($inet_family == AF_INET || $inet_family == AF_INET6) { 50 | if ($inet_family == AF_INET) { 51 | $daddr = ntop($sk->__sk_common.skc_daddr); 52 | $saddr = ntop($sk->__sk_common.skc_rcv_saddr); 53 | } else { 54 | $daddr = ntop($sk->__sk_common.skc_v6_daddr.in6_u.u6_addr8); 55 | $saddr = ntop($sk->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr8); 56 | } 57 | $lport = $sk->__sk_common.skc_num; 58 | $dport = $sk->__sk_common.skc_dport; 59 | 60 | // Destination port is big endian, it must be flipped 61 | $dport = ($dport >> 8) | (($dport << 8) & 0x00FF00); 62 | 63 | $state = $sk->__sk_common.skc_state; 64 | $statestr = @tcp_states[$state]; 65 | 66 | time("%H:%M:%S,"); 67 | printf("%d,%s,", pid, comm); 68 | printf("%s:%d,%s:%d,%s\n", $saddr, $lport, $daddr, $dport, $statestr); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/dashboards/tools/tcpretrans.bt: -------------------------------------------------------------------------------- 1 | /* 2 | * tcpretrans.bt Trace or count TCP retransmits 3 | * For Linux, uses bpftrace and eBPF. 4 | * 5 | * USAGE: tcpretrans.bt 6 | * 7 | * This is a bpftrace version of the bcc tool of the same name. 8 | * It is limited to ipv4 addresses, and doesn't support tracking TLPs. 9 | * 10 | * This uses dynamic tracing of kernel functions, and will need to be updated 11 | * to match kernel changes. 12 | * 13 | * Copyright (c) 2018 Dale Hamel. 14 | * Licensed under the Apache License, Version 2.0 (the "License") 15 | * 16 | * 23-Nov-2018 Dale Hamel created this. 17 | * 23-Aug-2019 Andreas Gerstmayr added CSV output 18 | */ 19 | // include: @output 20 | // table-retain-lines: 10 21 | 22 | #include 23 | #include 24 | 25 | BEGIN 26 | { 27 | printf("%s,%s,%s,%s,%s\n", "TIME", "PID", "LADDR:LPORT", 28 | "RADDR:RPORT", "STATE"); 29 | 30 | // See include/net/tcp_states.h: 31 | @tcp_states[1] = "ESTABLISHED"; 32 | @tcp_states[2] = "SYN_SENT"; 33 | @tcp_states[3] = "SYN_RECV"; 34 | @tcp_states[4] = "FIN_WAIT1"; 35 | @tcp_states[5] = "FIN_WAIT2"; 36 | @tcp_states[6] = "TIME_WAIT"; 37 | @tcp_states[7] = "CLOSE"; 38 | @tcp_states[8] = "CLOSE_WAIT"; 39 | @tcp_states[9] = "LAST_ACK"; 40 | @tcp_states[10] = "LISTEN"; 41 | @tcp_states[11] = "CLOSING"; 42 | @tcp_states[12] = "NEW_SYN_RECV"; 43 | } 44 | 45 | kprobe:tcp_retransmit_skb 46 | { 47 | $sk = (struct sock *)arg0; 48 | $inet_family = $sk->__sk_common.skc_family; 49 | 50 | if ($inet_family == AF_INET || $inet_family == AF_INET6) { 51 | // initialize variable type: 52 | $daddr = ntop(0); 53 | $saddr = ntop(0); 54 | if ($inet_family == AF_INET) { 55 | $daddr = ntop($sk->__sk_common.skc_daddr); 56 | $saddr = ntop($sk->__sk_common.skc_rcv_saddr); 57 | } else { 58 | $daddr = ntop( 59 | $sk->__sk_common.skc_v6_daddr.in6_u.u6_addr8); 60 | $saddr = ntop( 61 | $sk->__sk_common.skc_v6_rcv_saddr.in6_u.u6_addr8); 62 | } 63 | $lport = $sk->__sk_common.skc_num; 64 | $dport = $sk->__sk_common.skc_dport; 65 | 66 | // Destination port is big endian, it must be flipped 67 | $dport = ($dport >> 8) | (($dport << 8) & 0x00FF00); 68 | 69 | $state = $sk->__sk_common.skc_state; 70 | $statestr = @tcp_states[$state]; 71 | 72 | time("%H:%M:%S,"); 73 | printf("%d,%s:%d,%s:%d,%s\n", pid, $saddr, $lport, 74 | $daddr, $dport, $statestr); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/dashboards/tools/ustacks.bt: -------------------------------------------------------------------------------- 1 | /* 2 | * sample user stacks every 99 Hz, and clear map every 5 seconds 3 | * 4 | * 30-Oct-2019 Andreas Gerstmayr Created this. 5 | * 7-Aug-2020 Andreas Gerstmayr Added process name and PID. 6 | */ 7 | // include: @stacks 8 | // custom-output-block 9 | 10 | profile:hz:99 { @stacks[comm,pid,ustack] = count(); } 11 | 12 | interval:s:1 { 13 | print(@stacks); 14 | @cnt++; 15 | if (@cnt >= 5) { 16 | clear(@stacks); 17 | @cnt = 0; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/dashboards/tools/vfscount.bt: -------------------------------------------------------------------------------- 1 | /* 2 | * vfscount Count VFS calls ("vfs_*"). 3 | * For Linux, uses bpftrace and eBPF. 4 | * 5 | * Written as a basic example of counting kernel functions. 6 | * 7 | * USAGE: vfscount.bt 8 | * 9 | * This is a bpftrace version of the bcc tool of the same name. 10 | * 11 | * Copyright 2018 Netflix, Inc. 12 | * Licensed under the Apache License, Version 2.0 (the "License") 13 | * 14 | * 06-Sep-2018 Brendan Gregg Created this. 15 | */ 16 | 17 | kprobe:vfs_* 18 | { 19 | @[func] = count(); 20 | } 21 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/img/eBPF.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/datasources/bpftrace/img/eBPF.png -------------------------------------------------------------------------------- /src/datasources/bpftrace/module.ts: -------------------------------------------------------------------------------- 1 | import { DataSourcePlugin } from '@grafana/data'; 2 | import { BPFtraceQueryEditor } from './components/BPFtraceQueryEditor'; 3 | import { BPFtraceConfigEditor } from './configuration/BPFtraceConfigEditor'; 4 | import { PCPBPFtraceDataSource } from './datasource'; 5 | import { BPFtraceOptions, BPFtraceQuery } from './types'; 6 | 7 | export const plugin = new DataSourcePlugin(PCPBPFtraceDataSource) 8 | .setConfigEditor(BPFtraceConfigEditor) 9 | .setQueryEditor(BPFtraceQueryEditor); 10 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PCP bpftrace", 3 | "id": "performancecopilot-bpftrace-datasource", 4 | "type": "datasource", 5 | "category": "other", 6 | "metrics": true, 7 | "alerting": false, 8 | "annotations": false, 9 | "tables": true, 10 | "info": { 11 | "description": "system introspection using bpftrace scripts", 12 | "logos": { 13 | "small": "img/eBPF.png", 14 | "large": "img/eBPF.png" 15 | }, 16 | "links": [ 17 | { 18 | "name": "GitHub", 19 | "url": "https://github.com/performancecopilot/grafana-pcp" 20 | } 21 | ] 22 | }, 23 | "includes": [ 24 | { 25 | "type": "dashboard", 26 | "name": "PCP bpftrace: System Analysis", 27 | "path": "dashboards/pcp-bpftrace-system-analysis.json" 28 | }, 29 | { 30 | "type": "dashboard", 31 | "name": "PCP bpftrace: Flame Graphs", 32 | "path": "dashboards/pcp-bpftrace-flame-graphs.json" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/script.ts: -------------------------------------------------------------------------------- 1 | export enum MetricType { 2 | Control = 'control', 3 | Histogram = 'histogram', 4 | Output = 'output', 5 | Stacks = 'stacks', 6 | } 7 | 8 | export enum Status { 9 | Stopped = 'stopped', 10 | Starting = 'starting', 11 | Started = 'started', 12 | Stopping = 'stopping', 13 | Error = 'error', 14 | } 15 | 16 | export interface VariableDefinition { 17 | single: boolean; 18 | semantics: number; 19 | datatype: number; 20 | metrictype: MetricType; 21 | } 22 | 23 | export interface ScriptMetadata { 24 | name: string | null; 25 | include: string[] | null; 26 | table_retain_lines: number | null; 27 | } 28 | 29 | export interface State { 30 | status: Status; 31 | pid: number; 32 | exit_code: number; 33 | error: string; 34 | probes: number; 35 | } 36 | 37 | export interface Script { 38 | script_id: string; 39 | username: string | null; 40 | persistent: boolean; 41 | created_at: string; 42 | last_accessed_at: string; 43 | code: string; 44 | state: State; 45 | variables: Record; 46 | metadata: ScriptMetadata; 47 | } 48 | -------------------------------------------------------------------------------- /src/datasources/bpftrace/types.ts: -------------------------------------------------------------------------------- 1 | import { Optional } from 'utility-types'; 2 | import { MinimalPmapiQuery, PmapiOptions, PmapiQuery } from '../../datasources/lib/pmapi/types'; 3 | import { TargetFormat } from '../../datasources/lib/types'; 4 | import { Script } from './script'; 5 | 6 | export interface BPFtraceOptions extends PmapiOptions {} 7 | 8 | export interface BPFtraceQuery extends MinimalPmapiQuery {} 9 | 10 | export const defaultBPFtraceQuery: BPFtraceQuery & Optional = { 11 | refId: 'A', 12 | expr: '', 13 | format: TargetFormat.TimeSeries, 14 | options: { 15 | rateConversion: true, 16 | timeUtilizationConversion: true, 17 | }, 18 | }; 19 | 20 | export interface BPFtraceTargetData { 21 | script: Script; 22 | } 23 | -------------------------------------------------------------------------------- /src/datasources/lib/language.ts: -------------------------------------------------------------------------------- 1 | import { Monaco, MonacoType } from '../../components/monaco'; 2 | 3 | export interface TokenValue { 4 | offset: number; 5 | offsetEnd: number; 6 | type: string; 7 | value: string; 8 | } 9 | 10 | export function getTokenValues(monaco: Monaco, model: MonacoType.editor.ITextModel, position: MonacoType.Position) { 11 | const value = model.getValueInRange({ 12 | startLineNumber: 0, 13 | startColumn: 0, 14 | endLineNumber: position.lineNumber, 15 | endColumn: model.getLineMaxColumn(position.lineNumber), 16 | }); 17 | const tokens = monaco.editor.tokenize(value, model.getLanguageId()); 18 | 19 | const tokenValues: TokenValue[] = []; 20 | for (let line = 0; line < tokens.length; line++) { 21 | for (let i = 0; i < tokens[line].length; i++) { 22 | const offset = tokens[line][i].offset; 23 | const offsetEnd = i + 1 < tokens[line].length ? tokens[line][i + 1].offset : model.getLineLength(line + 1); // excluding 24 | if (line === tokens.length - 1 && offset >= position.column - 1) { 25 | break; 26 | } 27 | 28 | tokenValues.push({ 29 | offset, 30 | offsetEnd, 31 | type: tokens[line][i].type, 32 | value: model.getValueInRange({ 33 | startLineNumber: line + 1, 34 | endLineNumber: line + 1, 35 | startColumn: offset + 1, 36 | endColumn: offsetEnd + 1, 37 | }), 38 | }); 39 | } 40 | } 41 | return tokenValues; 42 | } 43 | 44 | export function findToken(tokens: TokenValue[], type: string) { 45 | for (let i = tokens.length - 1; i >= 0; i--) { 46 | if (tokens[i].type === type) { 47 | return tokens[i]; 48 | } 49 | } 50 | return; 51 | } 52 | -------------------------------------------------------------------------------- /src/datasources/lib/pmapi/field_transformations.test.ts: -------------------------------------------------------------------------------- 1 | import { FieldType, MutableDataFrame } from '@grafana/data'; 2 | import { Semantics } from '../../../common/types/pcp'; 3 | import { pmapiQuery } from '../specs/fixtures/datasource'; 4 | import { TargetFormat } from '../types'; 5 | import { applyFieldTransformations } from './field_transformations'; 6 | 7 | describe('field transformations', () => { 8 | it('should convert time based counters to time utilization', () => { 9 | const frame = new MutableDataFrame(); 10 | frame.addField({ name: 'Time', type: FieldType.time, values: [2000, 4000, 6000] }); 11 | // first rate: 1,000,000us = 1.0s per sec = CPU was busy 100% 12 | // second rate: 500,000us = 0.5s per sec = CPU was busy 50% 13 | frame.addField({ name: 'cgroup.cpu.stat.usage', type: FieldType.number, values: [1000000, 3000000, 4000000] }); 14 | 15 | const metadata = { 16 | name: 'cgroup.cpu.stat.usage', 17 | series: 'e750369c34fccb1f9c62554dcb9051ff5749b529', 18 | pmid: '3.67.2', 19 | indom: '3.16', 20 | type: 'u64', 21 | sem: Semantics.Counter, 22 | units: 'microsec', 23 | labels: { 24 | agent: 'proc', 25 | domainname: 'localdomain', 26 | hostname: 'agerstmayr-thinkpad', 27 | machineid: '6dabb302d60b402dabcc13dc4fd0fab8', 28 | }, 29 | 'text-oneline': 'CPU time consumed by processes in each cgroup', 30 | 'text-help': 'CPU time consumed by processes in each cgroup', 31 | }; 32 | applyFieldTransformations(pmapiQuery({ format: TargetFormat.TimeSeries }), metadata, frame); 33 | expect(frame).toMatchInlineSnapshot(` 34 | Object { 35 | "fields": Array [ 36 | Object { 37 | "config": Object {}, 38 | "labels": undefined, 39 | "name": "Time", 40 | "type": "time", 41 | "values": Array [ 42 | 2000, 43 | 4000, 44 | 6000, 45 | ], 46 | }, 47 | Object { 48 | "config": Object { 49 | "unit": "percentunit", 50 | }, 51 | "labels": undefined, 52 | "name": "cgroup.cpu.stat.usage", 53 | "type": "number", 54 | "values": Array [ 55 | undefined, 56 | 1, 57 | 0.5, 58 | ], 59 | }, 60 | ], 61 | "meta": undefined, 62 | "name": undefined, 63 | "refId": undefined, 64 | } 65 | `); 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /src/datasources/lib/pmapi/poller/types.ts: -------------------------------------------------------------------------------- 1 | import { Required } from 'utility-types'; 2 | import { Context, Indom, InstanceValue, Metadata } from '../../../../common/services/pmapi/types'; 3 | import { Labels } from '../../../../common/types/pcp'; 4 | import { Dict } from '../../../../common/types/utils'; 5 | import { PmapiQuery, Target } from '../types'; 6 | 7 | export interface Metric { 8 | metadata: Metadata; 9 | instanceDomain?: { 10 | // should be Dict, but JS objects don't support numbers 11 | instances: Dict; 12 | labels: Labels; 13 | }; 14 | values: InstanceValuesSnapshot[]; 15 | } 16 | 17 | export interface InstanceValuesSnapshot { 18 | timestampMs: number; 19 | values: InstanceValue[]; 20 | } 21 | 22 | export enum EndpointState { 23 | /** new entered endpoint, no context available */ 24 | PENDING, 25 | /** context available */ 26 | CONNECTED, 27 | } 28 | 29 | /** 30 | * single endpoint, identified by url and hostspec 31 | * each url/hostspec has a different context 32 | * each url/hostspec can have different metrics (and values) 33 | */ 34 | export interface Endpoint { 35 | state: EndpointState; 36 | url: string; 37 | hostspec: string; 38 | metrics: Metric[]; 39 | targets: Target[]; 40 | additionalMetricsToPoll: Array<{ name: string; callback: (values: InstanceValue[]) => void }>; 41 | errors: any[]; 42 | 43 | /** context, will be created at next poll */ 44 | context?: Context; 45 | /** backfilling with valkey */ 46 | hasValkey?: boolean; 47 | } 48 | 49 | export type EndpointWithCtx = Required; 50 | 51 | export interface QueryResult { 52 | endpoint: EndpointWithCtx; 53 | query: PmapiQuery; 54 | metrics: Metric[]; 55 | } 56 | -------------------------------------------------------------------------------- /src/datasources/lib/pmapi/types.ts: -------------------------------------------------------------------------------- 1 | import { DataQuery, DataSourceJsonData } from '@grafana/data'; 2 | import { MetricName } from '../../../common/types/pcp'; 3 | import { TargetFormat } from '../types'; 4 | 5 | export interface PmapiOptions extends DataSourceJsonData { 6 | hostspec?: string; 7 | retentionTime?: string; 8 | } 9 | 10 | export interface PmapiDefaultOptions { 11 | hostspec: string; 12 | retentionTime: string; 13 | } 14 | 15 | export interface PmapiQueryOptions { 16 | rateConversion: boolean; 17 | timeUtilizationConversion: boolean; 18 | } 19 | 20 | /** 21 | * query as stored in the dashboard JSON, all fields optional 22 | */ 23 | export interface MinimalPmapiQuery extends DataQuery { 24 | expr?: string; 25 | format?: TargetFormat; 26 | legendFormat?: string; 27 | options?: Partial; 28 | url?: string; 29 | hostspec?: string; 30 | } 31 | 32 | /** 33 | * query filled with all default values and 34 | * url + hostspec set from the panel or data source settings 35 | */ 36 | export interface PmapiQuery extends MinimalPmapiQuery { 37 | expr: string; 38 | format: TargetFormat; 39 | // legendFormat can still be undefined (not set) 40 | options: PmapiQueryOptions; 41 | url: string; 42 | hostspec: string; 43 | } 44 | 45 | export enum TargetState { 46 | /** newly entered target or target with error (trying again) */ 47 | PENDING, 48 | /** metrics exists and metadata available */ 49 | METRICS_AVAILABLE, 50 | /** fatal error, will not try again */ 51 | ERROR, 52 | } 53 | 54 | /** 55 | * Represents a target of a Grafana panel, which will be polled in the background 56 | * extends the Query (as provided by Grafana) with additional information: 57 | * - vector+bpftrace: errors occured while polling in the background, lastActive, ... 58 | * - vector: isDerivedMetric 59 | * - bpftrace: script 60 | * is persisted during multiple queries (key = targetId) 61 | */ 62 | export interface Target { 63 | targetId: string; 64 | state: TargetState; 65 | query: PmapiQuery; 66 | /** valid PCP metric names (can be a derived metric, e.g. derived_xxx), or multiple PCP metric names for bpftrace */ 67 | metricNames: MetricName[]; 68 | errors: any[]; 69 | lastActiveMs: number; 70 | custom?: T; 71 | } 72 | -------------------------------------------------------------------------------- /src/datasources/lib/specs/fixtures/datasource.ts: -------------------------------------------------------------------------------- 1 | import { defaults, defaultsDeep } from 'lodash'; 2 | import { DeepPartial } from 'utility-types'; 3 | import { MinimalPmapiQuery, PmapiQuery } from '../../../../datasources/lib/pmapi/types'; 4 | import { TargetFormat } from '../../../../datasources/lib/types'; 5 | 6 | export function query(props?: Partial): MinimalPmapiQuery { 7 | return defaults({}, props, { 8 | refId: 'A', 9 | expr: 'disk.dev.read', 10 | format: TargetFormat.TimeSeries, 11 | }); 12 | } 13 | 14 | export function pmapiQuery(props?: DeepPartial): PmapiQuery { 15 | return defaultsDeep({}, props, { 16 | ...query(props), 17 | url: '', 18 | hostspec: '', 19 | options: { 20 | rateConversion: true, 21 | timeUtilizationConversion: true, 22 | }, 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /src/datasources/lib/specs/fixtures/grafana.ts: -------------------------------------------------------------------------------- 1 | import { defaultsDeep } from 'lodash'; 2 | import { DeepPartial } from 'utility-types'; 3 | import { DataQuery, DataQueryRequest, dateTimeParse, TimeRange } from '@grafana/data'; 4 | import { FetchResponse } from '@grafana/runtime'; 5 | 6 | export function timeRange(): TimeRange { 7 | return { 8 | from: dateTimeParse(10000), 9 | to: dateTimeParse(20000), 10 | raw: { 11 | from: dateTimeParse(10000), 12 | to: dateTimeParse(20000), 13 | }, 14 | }; 15 | } 16 | 17 | export function dataQueryRequest(props?: DeepPartial>): DataQueryRequest { 18 | return defaultsDeep({}, props, { 19 | app: 'dashboard', 20 | dashboardId: 1, 21 | panelId: 2, 22 | requestId: '3', 23 | interval: '5s', 24 | intervalMs: 5000, 25 | startTime: 0, 26 | timezone: '', 27 | range: timeRange(), 28 | scopedVars: {}, 29 | targets: [], 30 | }); 31 | } 32 | 33 | export function response(data: T): FetchResponse { 34 | return { 35 | data, 36 | status: 200, 37 | statusText: 'Success', 38 | ok: true, 39 | headers: {} as any, 40 | redirected: false, 41 | type: 'default', 42 | url: '', 43 | config: {} as any, 44 | }; 45 | } 46 | -------------------------------------------------------------------------------- /src/datasources/lib/specs/fixtures/index.ts: -------------------------------------------------------------------------------- 1 | export * as ds from './datasource'; 2 | export * as grafana from './grafana'; 3 | export * as pcp from './pcp'; 4 | export * as pmapi from './pmapi'; 5 | export * as pmseries from './pmseries'; 6 | export * as poller from './poller'; 7 | -------------------------------------------------------------------------------- /src/datasources/lib/specs/fixtures/pmapi.ts: -------------------------------------------------------------------------------- 1 | import { pcp } from '.'; 2 | import { 3 | Indom, 4 | InstanceId, 5 | PmapiContextResponse, 6 | PmapiDeriveResponse, 7 | PmapiFetchResponse, 8 | PmapiIndomResponse, 9 | PmapiMetricResponse, 10 | PmapiStoreResponse, 11 | } from '../../../../common/services/pmapi/types'; 12 | import { MetricName } from '../../../../common/types/pcp'; 13 | 14 | export function context(ctx = 123): PmapiContextResponse { 15 | return { context: ctx, labels: {} }; 16 | } 17 | 18 | export function metric(metrics: MetricName[]): PmapiMetricResponse { 19 | return { 20 | metrics: metrics.map(metric => pcp.metrics[metric].metadata), 21 | }; 22 | } 23 | 24 | export function indom(metric: MetricName): PmapiIndomResponse { 25 | const pollerIndom = pcp.metrics[metric].instanceDomain!; 26 | return { 27 | instances: Object.values(pollerIndom.instances) as Indom[], 28 | labels: pollerIndom.labels, 29 | }; 30 | } 31 | 32 | export function fetch( 33 | metric: string, 34 | timestamp: number, 35 | instances: Array<[InstanceId | null, number]> 36 | ): PmapiFetchResponse { 37 | return { 38 | timestamp, 39 | values: [ 40 | { 41 | name: metric, 42 | instances: instances.map(([instance, value]) => ({ instance, value })), 43 | }, 44 | ], 45 | }; 46 | } 47 | 48 | export function store(success = true): PmapiStoreResponse { 49 | return { success }; 50 | } 51 | 52 | export function derive(success = true): PmapiDeriveResponse { 53 | return { success }; 54 | } 55 | -------------------------------------------------------------------------------- /src/datasources/lib/specs/fixtures/poller.ts: -------------------------------------------------------------------------------- 1 | import { defaultsDeep } from 'lodash'; 2 | import { DeepPartial } from 'utility-types'; 3 | import { ds } from '.'; 4 | import { EndpointState, EndpointWithCtx } from '../../../../datasources/lib/pmapi/poller/types'; 5 | import { Target, TargetState } from '../../../../datasources/lib/pmapi/types'; 6 | 7 | export function endpoint(props?: DeepPartial): EndpointWithCtx { 8 | return defaultsDeep({}, props, { 9 | state: EndpointState.CONNECTED, 10 | url: 'http://fixture_url:1234', 11 | hostspec: '', 12 | metrics: [], 13 | targets: [], 14 | additionalMetricsToPoll: [], 15 | errors: [], 16 | context: { 17 | context: 123, 18 | labels: {}, 19 | }, 20 | }); 21 | } 22 | 23 | export function target(props?: DeepPartial): Target { 24 | const query = ds.pmapiQuery(props?.query); 25 | return defaultsDeep({}, props, { 26 | targetId: `0/1/${query.refId ?? 'A'}`, 27 | state: TargetState.METRICS_AVAILABLE, 28 | query, 29 | metricNames: [query.expr], 30 | errors: [], 31 | lastActiveMs: 0, 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/datasources/lib/specs/mocks/backend_srv.ts: -------------------------------------------------------------------------------- 1 | import { getLogger } from 'loglevel'; 2 | import { of } from 'rxjs'; 3 | import { BackendSrv } from '@grafana/runtime'; 4 | import * as grafana from '../../../../datasources/lib/specs/fixtures/grafana'; 5 | 6 | const backendSrvMockLogger = getLogger('backendSrvMock'); 7 | export const backendSrvMock: jest.Mocked = { 8 | fetch: jest.fn(), 9 | } as any; 10 | 11 | export function mockNextResponse(response: T) { 12 | backendSrvMock.fetch.mockImplementationOnce(request => { 13 | backendSrvMockLogger.debug('fetch', { 14 | request: { 15 | url: request.url, 16 | params: request.params, 17 | }, 18 | response: JSON.stringify(response), 19 | }); 20 | return of(grafana.response(response)); 21 | }); 22 | } 23 | 24 | export function mockNextResponses(responses: any[]) { 25 | for (const response of responses) { 26 | mockNextResponse(response); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/datasources/lib/types.ts: -------------------------------------------------------------------------------- 1 | export enum TargetFormat { 2 | TimeSeries = 'time_series', 3 | Heatmap = 'heatmap', 4 | Geomap = 'geomap', 5 | /** vector only */ 6 | MetricsTable = 'metrics_table', 7 | /** bpftrace only */ 8 | CsvTable = 'csv_table', 9 | /** bpftrace only */ 10 | FlameGraph = 'flamegraph', 11 | } 12 | -------------------------------------------------------------------------------- /src/datasources/valkey/README.md: -------------------------------------------------------------------------------- 1 | # PCP Valkey 2 | 3 | #### [Data source documentation](https://grafana-pcp.readthedocs.io/en/latest/datasources/valkey.html) 4 | -------------------------------------------------------------------------------- /src/datasources/valkey/components/language/PmseriesBuiltins.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": [ 3 | { 4 | "def": "max(expr)", 5 | "doc": "the maximum value in the time series for each instance of expr" 6 | }, 7 | { 8 | "def": "min(expr)", 9 | "doc": "the minimum value in the time series for each instance of expr" 10 | }, 11 | { 12 | "def": "rate(expr)", 13 | "doc": "the rate with respect to time of each sample. The given expr must have counter semantics and the result will have instant semantics (the time dimension reduced by one). In addition, the result will have one less sample than the operand - this is because the first sample cannot be rate converted (two samples are required)." 14 | }, 15 | { 16 | "def": "rescale(expr, scale)", 17 | "doc": "rescale the values in the time series for each instance of expr to scale (units). Note that expr should have instant or discrete semantics (not counter - rate conversion should be done first if needed). The time, space and count dimensions between expr and scale must be compatible. Example: rate convert the read throughput counter for each disk instance and then rescale to mbytes per second. Note the native units of disk.dev.read_bytes is a counter of kbytes read from each device instance since boot: `rescale(rate(disk.dev.read_bytes), \"mbytes/s\")`" 18 | }, 19 | { 20 | "def": "abs(expr)", 21 | "doc": "the absolute value of each value in the time series for each instance of expr. This has no effect if the type of expr is unsigned." 22 | }, 23 | { 24 | "def": "floor(expr)", 25 | "doc": "rounded down to the nearest integer value of the time series for each instance of expr." 26 | }, 27 | { 28 | "def": "round(expr)", 29 | "doc": "rounded up or down to the nearest integer for each value in the time series for each instance of expr." 30 | }, 31 | { 32 | "def": "log(expr)", 33 | "doc": "logarithm of the values in the time series for each instance of expr" 34 | }, 35 | { 36 | "def": "sqrt(expr)", 37 | "doc": "square root of the values in the time series for each instance of expr" 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/datasources/valkey/configuration/ValkeyConfigEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DataSourcePluginOptionsEditorProps } from '@grafana/data'; 3 | import { DataSourceHttpSettings } from '@grafana/ui'; 4 | import { ValkeyOptions } from '../types'; 5 | 6 | export type Props = DataSourcePluginOptionsEditorProps; 7 | 8 | export const ValkeyConfigEditor = (props: Props) => { 9 | const { options, onOptionsChange } = props; 10 | 11 | return ( 12 | <> 13 | 19 | 20 | ); 21 | }; 22 | -------------------------------------------------------------------------------- /src/datasources/valkey/img/valkey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/datasources/valkey/img/valkey.png -------------------------------------------------------------------------------- /src/datasources/valkey/module.ts: -------------------------------------------------------------------------------- 1 | import { DataSourcePlugin } from '@grafana/data'; 2 | import { ValkeyQueryEditor } from './components/ValkeyQueryEditor'; 3 | import { ValkeyConfigEditor } from './configuration/ValkeyConfigEditor'; 4 | import { PCPValkeyDataSource } from './datasource'; 5 | import { ValkeyOptions, ValkeyQuery } from './types'; 6 | 7 | export const plugin = new DataSourcePlugin(PCPValkeyDataSource) 8 | .setConfigEditor(ValkeyConfigEditor) 9 | .setQueryEditor(ValkeyQueryEditor); 10 | -------------------------------------------------------------------------------- /src/datasources/valkey/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PCP Valkey", 3 | "id": "performancecopilot-valkey-datasource", 4 | "type": "datasource", 5 | "category": "tsdb", 6 | "metrics": true, 7 | "alerting": true, 8 | "annotations": false, 9 | "tables": true, 10 | "backend": true, 11 | "executable": "pcp_valkey_datasource", 12 | "info": { 13 | "description": "fast, scalable time series aggregation across multiple hosts", 14 | "logos": { 15 | "small": "img/valkey.png", 16 | "large": "img/valkey.png" 17 | }, 18 | "links": [ 19 | { 20 | "name": "GitHub", 21 | "url": "https://github.com/performancecopilot/grafana-pcp" 22 | } 23 | ] 24 | }, 25 | "includes": [ 26 | { 27 | "type": "dashboard", 28 | "name": "PCP Valkey: Host Overview", 29 | "path": "dashboards/pcp-valkey-host-overview.json" 30 | }, 31 | { 32 | "type": "dashboard", 33 | "name": "PCP Valkey: Microsoft SQL Server", 34 | "path": "dashboards/pcp-valkey-mssql-server.json" 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /src/datasources/valkey/types.ts: -------------------------------------------------------------------------------- 1 | import { DataQuery, DataSourceJsonData } from '@grafana/data'; 2 | import { TargetFormat } from '../../datasources/lib/types'; 3 | 4 | export interface ValkeyOptions extends DataSourceJsonData {} 5 | 6 | export interface ValkeyQueryOptions { 7 | rateConversion: boolean; 8 | timeUtilizationConversion: boolean; 9 | } 10 | 11 | export interface ValkeyQuery extends DataQuery { 12 | expr: string; 13 | format: TargetFormat; 14 | legendFormat?: string; 15 | options?: Partial; 16 | } 17 | 18 | export const defaultValkeyQuery: Partial = { 19 | expr: '', 20 | format: TargetFormat.TimeSeries, 21 | options: { 22 | rateConversion: true, 23 | timeUtilizationConversion: true, 24 | }, 25 | }; 26 | -------------------------------------------------------------------------------- /src/datasources/vector/DEV_NOTES.md: -------------------------------------------------------------------------------- 1 | # Why doesn't the Vector data source use the streaming functionality of Grafana? 2 | - the Vector data source is not streaming, but polling 3 | - streaming disables the auto-refresh selection, so the refresh rate would be fixed in the data source settings 4 | - Grafana doesn't implement any metrics cache for streaming data sources, so the same metrics cache as now is required anyway 5 | - Grafana stops refreshing the dashboard when the tab is in background, i.e. streaming would stop - but polling with a JS timer still works 6 | -------------------------------------------------------------------------------- /src/datasources/vector/README.md: -------------------------------------------------------------------------------- 1 | # PCP Vector 2 | 3 | #### [Data source documentation](https://grafana-pcp.readthedocs.io/en/latest/datasources/vector.html) 4 | -------------------------------------------------------------------------------- /src/datasources/vector/components/language/PmapiBuiltins.json: -------------------------------------------------------------------------------- 1 | { 2 | "functions": [ 3 | { 4 | "def": "avg(x)", 5 | "doc": "A singular instance being the average value across all instances for the metric x." 6 | }, 7 | { 8 | "def": "count(x)", 9 | "doc": "A singular instance being the count of the number of instances for the metric x. As a special case, if fetching the metric x returns an error, then count(x) will be 0." 10 | }, 11 | { 12 | "def": "defined(x)", 13 | "doc": "A boolean value that is true (`1`) if the metric x is defined in the PMNS, else false (`0`). The function is evaluated when a new PMAPI context is created with pmNewContext(3) or re-established with pmReconnectContext(3). So any subsequent changes to the PMNS after the PMAPI context has been established will not change the value of this function in the expression evaluation." 14 | }, 15 | { 16 | "def": "delta(x)", 17 | "doc": "Returns the difference in values for the metric x between one call to pmFetch(3) and the next. There is one value in the result for each instance that appears in both the current and the previous sample." 18 | }, 19 | { 20 | "def": "rate(x)", 21 | "doc": "Returns the difference in values for the metric x between one call to pmFetch(3) and the next divided by the elapsed time between the calls to pmFetch(3). The semantics of the derived metric are based on the semantics of the operand (x) with the dimension in the **time** domain decreased by one and scaling if required in the time utilization case where the operand is in units of time, and the derived metric is unitless. This mimics the rate conversion applied to counter metrics by tools such as pmval(1), pmie(1) and pmchart(1). There is one value in the result for each instance that appears in both the current and the previous sample." 22 | }, 23 | { 24 | "def": "instant(x)", 25 | "doc": "Returns the current value of the metric x, even it has the semantics of a counter, i.e. PM_SEM_COUNTER. The semantics of the derived metric are based on the semantics of the operand (x); if x has semantics PM_SEM_COUNTER, the semantics of instant(x) is PM_SEM_INSTANT, otherwise the semantics of the derived metric is the same as the semantics of the metric x." 26 | }, 27 | { 28 | "def": "max(x)", 29 | "doc": "A singular instance being the maximum value across all instances for the metric x." 30 | }, 31 | { 32 | "def": "min(x)", 33 | "doc": "A singular instance being the minimum value across all instances for the metric x." 34 | }, 35 | { 36 | "def": "sum(x)", 37 | "doc": "A singular instance being the sum of the values across all instances for the metric x." 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /src/datasources/vector/config.ts: -------------------------------------------------------------------------------- 1 | export const Config = { 2 | /** default refresh interval if not specified in the URL query params */ 3 | defaultRefreshIntervalMs: 1000, 4 | 5 | /** set timeout to 6s, because pmproxy timeout is 5s (e.g. connecting to a remote pmcd) */ 6 | apiTimeoutMs: 6000, 7 | 8 | /** 9 | * don't remove targets immediately if not requested in refreshInterval 10 | * also instruct pmproxy to not clear the context immediately if not used in refreshInterval 11 | */ 12 | gracePeriodMs: 10000, 13 | 14 | defaults: { 15 | hostspec: 'pcp://127.0.0.1', 16 | retentionTime: '30m', 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /src/datasources/vector/dashboards/pcp-vector-top-consumers.jsonnet: -------------------------------------------------------------------------------- 1 | local grafana = import 'grafonnet/grafana.libsonnet'; 2 | 3 | grafana.dashboard.new( 4 | 'PCP Vector: Top Consumers', 5 | tags=['pcp-vector'], 6 | time_from='now-5m', 7 | time_to='now', 8 | refresh='5s', 9 | ) 10 | .addTemplate( 11 | grafana.template.datasource( 12 | 'datasource', 13 | 'performancecopilot-vector-datasource', 14 | 'PCP Vector', 15 | ) 16 | ) 17 | .addPanel( 18 | grafana.text.new( 19 | 'Configuration Instructions', 20 | mode='markdown', 21 | content='This dashboard requires [authentication](https://grafana-pcp.readthedocs.io/en/latest/datasources/authentication.html) to be set up for the PCP Vector datasource. The "Top Network Consumers" table requires the [bcc PMDA](https://man7.org/linux/man-pages/man1/pmdabcc.1.html) to be installed and configured with the netproc module.', 22 | ), gridPos={ 23 | x: 0, 24 | y: 0, 25 | w: 24, 26 | h: 2, 27 | } 28 | ) 29 | .addPanel( 30 | grafana.tablePanel.new( 31 | 'Top CPU consumers', 32 | datasource='$datasource', 33 | styles=null, 34 | ) 35 | .addTargets([ 36 | { expr: 'proc.hog.cpu', format: 'metrics_table', legendFormat: '$metric' }, 37 | ]) + { options+: {sortBy: [{desc: true, displayName: 'proc.hog.cpu'}]}}, gridPos={ 38 | x: 0, 39 | y: 2, 40 | w: 12, 41 | h: 8, 42 | } 43 | ) 44 | .addPanel( 45 | grafana.tablePanel.new( 46 | 'Top Memory Consumers', 47 | datasource='$datasource', 48 | styles=null, 49 | ) 50 | .addTargets([ 51 | { expr: 'proc.hog.mem', format: 'metrics_table', legendFormat: '$metric' }, 52 | ]) + { options+: {sortBy: [{desc: true, displayName: 'proc.hog.mem'}]}}, gridPos={ 53 | x: 12, 54 | y: 2, 55 | w: 12, 56 | h: 8, 57 | } 58 | ) 59 | .addPanel( 60 | grafana.tablePanel.new( 61 | 'Top Disk Consumers', 62 | datasource='$datasource', 63 | styles=null, 64 | ) 65 | .addTargets([ 66 | { expr: 'proc.hog.disk', format: 'metrics_table', legendFormat: '$metric' }, 67 | ]) + { options+: {sortBy: [{desc: true, displayName: 'proc.hog.disk'}]}}, gridPos={ 68 | x: 0, 69 | y: 10, 70 | w: 12, 71 | h: 8, 72 | } 73 | ) 74 | .addPanel( 75 | grafana.tablePanel.new( 76 | 'Top Network Consumers', 77 | datasource='$datasource', 78 | styles=null, 79 | ) 80 | .addTargets([ 81 | { expr: 'proc.hog.net', format: 'metrics_table', legendFormat: '$metric' }, 82 | ]) + { options+: {sortBy: [{desc: true, displayName: 'proc.hog.net'}]}}, gridPos={ 83 | x: 12, 84 | y: 10, 85 | w: 12, 86 | h: 8, 87 | } 88 | ) -------------------------------------------------------------------------------- /src/datasources/vector/img/vector.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/datasources/vector/img/vector.png -------------------------------------------------------------------------------- /src/datasources/vector/module.ts: -------------------------------------------------------------------------------- 1 | import { DataSourcePlugin } from '@grafana/data'; 2 | import { VectorQueryEditor } from './components/VectorQueryEditor'; 3 | import { VectorConfigEditor } from './configuration/VectorConfigEditor'; 4 | import { PCPVectorDataSource } from './datasource'; 5 | import { VectorOptions, VectorQuery } from './types'; 6 | 7 | export const plugin = new DataSourcePlugin(PCPVectorDataSource) 8 | .setConfigEditor(VectorConfigEditor) 9 | .setQueryEditor(VectorQueryEditor); 10 | -------------------------------------------------------------------------------- /src/datasources/vector/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "PCP Vector", 3 | "id": "performancecopilot-vector-datasource", 4 | "type": "datasource", 5 | "category": "tsdb", 6 | "metrics": true, 7 | "alerting": false, 8 | "annotations": false, 9 | "tables": true, 10 | "info": { 11 | "description": "on-demand performance monitoring with container support", 12 | "logos": { 13 | "small": "img/vector.png", 14 | "large": "img/vector.png" 15 | }, 16 | "links": [ 17 | { 18 | "name": "GitHub", 19 | "url": "https://github.com/performancecopilot/grafana-pcp" 20 | } 21 | ] 22 | }, 23 | "includes": [ 24 | { 25 | "type": "dashboard", 26 | "name": "PCP Vector: Host Overview", 27 | "path": "dashboards/pcp-vector-host-overview.json" 28 | }, 29 | { 30 | "type": "dashboard", 31 | "name": "PCP Vector: Container Overview (CGroups v1)", 32 | "path": "dashboards/pcp-vector-container-overview-cgroups1.json" 33 | }, 34 | { 35 | "type": "dashboard", 36 | "name": "PCP Vector: Container Overview (CGroups v2)", 37 | "path": "dashboards/pcp-vector-container-overview-cgroups2.json" 38 | }, 39 | { 40 | "type": "dashboard", 41 | "name": "PCP Vector: eBPF/BCC Overview", 42 | "path": "dashboards/pcp-vector-bcc-overview.json" 43 | }, 44 | { 45 | "type": "dashboard", 46 | "name": "PCP Vector: Microsoft SQL Server", 47 | "path": "dashboards/pcp-vector-mssql-server.json" 48 | }, 49 | { 50 | "type": "dashboard", 51 | "name": "PCP Vector: UWSGI Overview", 52 | "path": "dashboards/pcp-vector-uwsgi-overview.json" 53 | }, 54 | { 55 | "type": "dashboard", 56 | "name": "PCP Vector: Top Consumers", 57 | "path": "dashboards/pcp-vector-top-consumers.json" 58 | } 59 | ] 60 | } 61 | -------------------------------------------------------------------------------- /src/datasources/vector/types.ts: -------------------------------------------------------------------------------- 1 | import { Optional } from 'utility-types'; 2 | import { MinimalPmapiQuery, PmapiOptions, PmapiQuery } from '../../datasources/lib/pmapi/types'; 3 | import { TargetFormat } from '../../datasources/lib/types'; 4 | 5 | export interface VectorOptions extends PmapiOptions {} 6 | 7 | export interface VectorQuery extends MinimalPmapiQuery {} 8 | 9 | export const defaultVectorQuery: VectorQuery & Optional = { 10 | refId: 'A', 11 | expr: '', 12 | format: TargetFormat.TimeSeries, 13 | options: { 14 | rateConversion: true, 15 | timeUtilizationConversion: true, 16 | }, 17 | }; 18 | 19 | export interface VectorTargetData {} 20 | -------------------------------------------------------------------------------- /src/img/screenshots/bpftrace-cpu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/bpftrace-cpu.png -------------------------------------------------------------------------------- /src/img/screenshots/bpftrace-disk.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/bpftrace-disk.png -------------------------------------------------------------------------------- /src/img/screenshots/bpftrace-flame-graph.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/bpftrace-flame-graph.png -------------------------------------------------------------------------------- /src/img/screenshots/bpftrace-function-autocompletion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/bpftrace-function-autocompletion.png -------------------------------------------------------------------------------- /src/img/screenshots/bpftrace-probe-autocompletion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/bpftrace-probe-autocompletion.png -------------------------------------------------------------------------------- /src/img/screenshots/bpftrace-tcp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/bpftrace-tcp.png -------------------------------------------------------------------------------- /src/img/screenshots/bpftrace-variable-autocompletion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/bpftrace-variable-autocompletion.png -------------------------------------------------------------------------------- /src/img/screenshots/search.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/search.png -------------------------------------------------------------------------------- /src/img/screenshots/vector-bcc-overview1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/vector-bcc-overview1.png -------------------------------------------------------------------------------- /src/img/screenshots/vector-bcc-overview2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/vector-bcc-overview2.png -------------------------------------------------------------------------------- /src/img/screenshots/vector-checklist.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/vector-checklist.png -------------------------------------------------------------------------------- /src/img/screenshots/vector-containers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/vector-containers.png -------------------------------------------------------------------------------- /src/img/screenshots/vector-metric-autocompletion.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/vector-metric-autocompletion.png -------------------------------------------------------------------------------- /src/img/screenshots/vector-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/performancecopilot/grafana-pcp/9ff637e33fa1ffcb0e6595a6f2249f493baabcdb/src/img/screenshots/vector-overview.png -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { AppPlugin } from '@grafana/data'; 2 | import { AppConfig } from './components/appconfig/config'; 3 | import { AppSettings } from './components/appconfig/types'; 4 | import { App } from './components/app/App'; 5 | 6 | export const plugin = new AppPlugin() 7 | .addConfigPage({ id: 'config', title: 'Config', icon: 'cog', body: AppConfig }) 8 | .setRootPage(App); 9 | -------------------------------------------------------------------------------- /src/panels/breadcrumbs/module.tsx: -------------------------------------------------------------------------------- 1 | import { PanelPlugin } from '@grafana/data'; 2 | import BreadcrumbsPanel from './BreadcrumbsPanel'; 3 | import { Options } from './types'; 4 | 5 | export const plugin = new PanelPlugin(BreadcrumbsPanel); 6 | -------------------------------------------------------------------------------- /src/panels/breadcrumbs/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "name": "PCP Breadcrumbs", 4 | "id": "performancecopilot-breadcrumbs-panel", 5 | "skipDataQuery": true, 6 | "info": { 7 | "description": "Grafana Panel for displaying breadcrumbs navigation in PCP checklist overview dashboards", 8 | "logos": { 9 | "small": "img/pcp-logo.svg", 10 | "large": "img/pcp-logo.svg" 11 | }, 12 | "links": [ 13 | { 14 | "name": "GitHub", 15 | "url": "https://github.com/performancecopilot/grafana-pcp" 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/panels/breadcrumbs/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | import { GrafanaTheme } from '@grafana/data'; 3 | 4 | export const breadcrumbsContainer = (theme: GrafanaTheme) => css` 5 | display: flex; 6 | height: 100%; 7 | justify-content: flex-start; 8 | align-items: center; 9 | margin-left: ${theme.spacing.sm}; 10 | margin-right: ${theme.spacing.sm}; 11 | `; 12 | 13 | export const breadcrumbsList = (theme: GrafanaTheme) => css` 14 | display: flex; 15 | height: 100%; 16 | justify-content: flex-start; 17 | align-items: center; 18 | list-style-type: none; 19 | position: relative; 20 | z-index: ${theme.zIndex.dropdown - 100}; 21 | `; 22 | 23 | export const breadcrumbsItem = (theme: GrafanaTheme) => css` 24 | & + & { 25 | margin-left: ${theme.spacing.sm}; 26 | } 27 | `; 28 | 29 | export const breadcrumbsCurrentItem = (theme: GrafanaTheme) => css` 30 | position: relative; 31 | &:before { 32 | content: ''; 33 | position: absolute; 34 | top: -4px; 35 | left: -4px; 36 | width: calc(100% + 8px); 37 | height: calc(100% + 8px); 38 | border: 2px solid ${theme.colors.formInputBorderActive}; 39 | border-radius: ${theme.border.radius.md}; 40 | opacity: 0.25; 41 | } 42 | `; 43 | 44 | export const breadcrumbsBtn = (theme: GrafanaTheme) => css` 45 | padding-left: ${theme.spacing.sm}; 46 | padding-right: ${theme.spacing.sm}; 47 | `; 48 | 49 | export const breadcrumbsControl = (theme: GrafanaTheme) => css` 50 | min-width: 150px; 51 | `; 52 | 53 | export const notUsableContainer = (width: number, height: number) => css` 54 | display: flex; 55 | justify-content: center; 56 | align-items: center; 57 | width: ${width}px; 58 | height: ${height}px; 59 | `; 60 | -------------------------------------------------------------------------------- /src/panels/breadcrumbs/types.ts: -------------------------------------------------------------------------------- 1 | export interface LinkItem { 2 | title: string; 3 | name: string; 4 | uid: string; 5 | active?: boolean; 6 | current?: boolean; 7 | } 8 | 9 | export interface Options { 10 | items: LinkItem[][]; 11 | } 12 | -------------------------------------------------------------------------------- /src/panels/flamegraph/FlameGraphPanel.tsx: -------------------------------------------------------------------------------- 1 | import memoizeOne from 'memoize-one'; 2 | import React, { PureComponent } from 'react'; 3 | import { dateTime, PanelProps } from '@grafana/data'; 4 | import { Tooltip } from '@grafana/ui'; 5 | import { FlameGraphChart } from './FlameGraphChart'; 6 | import { generateFlameGraphModel } from './model'; 7 | import { Options } from './types'; 8 | 9 | export class FlameGraphPanel extends PureComponent> { 10 | computeModel = memoizeOne(generateFlameGraphModel); 11 | 12 | render() { 13 | const model = this.computeModel(this.props.data, this.props.options); 14 | if (model.root.children.length === 0) { 15 | return ( 16 |
17 | 18 | Sampling, waiting for data... 19 | 20 | 21 | 22 | 23 |
24 | ); 25 | } 26 | 27 | const fromDate = dateTime(model.minDate).format('HH:mm:ss'); 28 | const toDate = dateTime(model.maxDate).format('HH:mm:ss'); 29 | const title = `${fromDate} - ${toDate}`; 30 | return ( 31 | 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/panels/flamegraph/css/flamegraph.css: -------------------------------------------------------------------------------- 1 | .theme-dark svg.d3-flame-graph rect { 2 | stroke: rgb(33, 33, 36); 3 | } 4 | 5 | .flamegraph-bar { 6 | display: flex; 7 | height: 40px; 8 | align-items: center; 9 | margin-bottom: 10px; 10 | } 11 | 12 | .flamegraph-bar .left { 13 | flex: 1; 14 | justify-content: flex-start; 15 | } 16 | 17 | .flamegraph-bar .center { 18 | display: flex; 19 | flex: 1; 20 | justify-content: center; 21 | } 22 | 23 | .flamegraph-bar .right { 24 | display: flex; 25 | flex: 1; 26 | justify-content: flex-end; 27 | } 28 | 29 | .flamegraph-bar .date { 30 | font-size: 12px; 31 | } 32 | 33 | 34 | .flamegraph-bar button { 35 | margin: 0 4px; 36 | } 37 | 38 | .flamegraph-bar label { 39 | margin-top: -2px; 40 | } 41 | -------------------------------------------------------------------------------- /src/panels/flamegraph/model.ts: -------------------------------------------------------------------------------- 1 | import { StackFrame } from 'd3-flame-graph'; 2 | import { FieldType, getTimeField, MISSING_VALUE, PanelData } from '@grafana/data'; 3 | import { Options } from './types'; 4 | 5 | interface FlameGraphModel { 6 | root: StackFrame; 7 | minDate: number; 8 | maxDate: number; 9 | } 10 | 11 | function readStacks(root: StackFrame, options: Options, instanceName: string, count: number) { 12 | let curNode = root; 13 | const stacks = instanceName.split(/[\n,]/); 14 | for (let stackFrame of stacks) { 15 | stackFrame = stackFrame.trim(); 16 | if (!stackFrame || (options.hideUnresolvedStackFrames && stackFrame.startsWith('0x'))) { 17 | continue; 18 | } 19 | if (options.hideIdleStacks && stackFrame.startsWith('cpuidle_enter_state+')) { 20 | return; 21 | } 22 | 23 | let child = curNode.children.find(child => child.name === stackFrame); 24 | if (!child) { 25 | child = { name: stackFrame, value: 0, children: [] }; 26 | curNode.children.push(child); 27 | } 28 | curNode = child; 29 | } 30 | curNode.value = count; 31 | } 32 | 33 | export function generateFlameGraphModel(panelData: PanelData, options: Options): FlameGraphModel { 34 | const model: FlameGraphModel = { 35 | root: { name: 'root', value: 0, children: [] }, 36 | minDate: 0, 37 | maxDate: 0, 38 | }; 39 | 40 | if (panelData.series.length !== 1) { 41 | return model; 42 | } 43 | 44 | const dataFrame = panelData.series[0]; 45 | const { timeField } = getTimeField(dataFrame); 46 | if (!timeField || dataFrame.length === 0) { 47 | return model; 48 | } 49 | 50 | model.minDate = timeField.values.get(0); 51 | model.maxDate = timeField.values.get(timeField.values.length - 1); 52 | 53 | for (const field of dataFrame.fields) { 54 | if (field.type !== FieldType.number || !field.config.custom?.instance?.name) { 55 | continue; 56 | } 57 | 58 | // each dataframe (stack) is a rate-converted counter 59 | // sum all rates in the selected time frame 60 | let count = 0; 61 | for (let i = 0; i < field.values.length; i++) { 62 | const value = field.values.get(i); 63 | if (value !== MISSING_VALUE) { 64 | count += value; 65 | } 66 | } 67 | if (count < options.minSamples) { 68 | continue; 69 | } 70 | 71 | readStacks(model.root, options, field.config.custom?.instance?.name, count); 72 | } 73 | 74 | return model; 75 | } 76 | -------------------------------------------------------------------------------- /src/panels/flamegraph/module.tsx: -------------------------------------------------------------------------------- 1 | import { PanelPlugin } from '@grafana/data'; 2 | import { FlameGraphPanel } from './FlameGraphPanel'; 3 | import { defaults, Options } from './types'; 4 | 5 | export const plugin = new PanelPlugin(FlameGraphPanel).setPanelOptions(builder => 6 | builder 7 | .addNumberInput({ 8 | path: 'minSamples', 9 | name: 'Min samples', 10 | defaultValue: defaults.minSamples, 11 | }) 12 | .addBooleanSwitch({ 13 | path: 'hideUnresolvedStackFrames', 14 | name: 'Hide unresolved', 15 | defaultValue: defaults.hideUnresolvedStackFrames, 16 | }) 17 | .addBooleanSwitch({ 18 | path: 'hideIdleStacks', 19 | name: 'Hide idle stacks', 20 | defaultValue: defaults.hideIdleStacks, 21 | }) 22 | ); 23 | -------------------------------------------------------------------------------- /src/panels/flamegraph/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "name": "PCP Flame Graph", 4 | "id": "performancecopilot-flamegraph-panel", 5 | "info": { 6 | "description": "Grafana Panel for displaying Flame Graphs", 7 | "logos": { 8 | "small": "img/flamegraph.svg", 9 | "large": "img/flamegraph.svg" 10 | }, 11 | "links": [ 12 | { 13 | "name": "GitHub", 14 | "url": "https://github.com/performancecopilot/grafana-pcp" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/panels/flamegraph/types.ts: -------------------------------------------------------------------------------- 1 | export interface Options { 2 | minSamples: number; 3 | hideUnresolvedStackFrames: boolean; 4 | hideIdleStacks: boolean; 5 | } 6 | 7 | export const defaults: Options = { 8 | minSamples: 1, 9 | hideUnresolvedStackFrames: false, 10 | hideIdleStacks: false, 11 | }; 12 | -------------------------------------------------------------------------------- /src/panels/troubleshooting/TroubleshootingPanel.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { PanelProps } from '@grafana/data'; 3 | import { TimeSeries, TooltipPlugin, ZoomPlugin } from '@grafana/ui'; 4 | import { TroubleshootingPane } from './TroubleshootingPane'; 5 | import { graphWrapper, notUsableContainer } from './styles'; 6 | import { Options } from './types'; 7 | 8 | interface Props extends PanelProps {} 9 | 10 | export const TroubleshootingPanel: React.FC = (props: Props) => { 11 | const { data, timeRange, timeZone, width, height, options, onChangeTimeRange } = props; 12 | if (!options.troubleshooting) { 13 | return ( 14 |
15 |

The PCP Troubleshooting panel is not intended for use in user defined dashboards.

16 |
17 | ); 18 | } 19 | 20 | const dataAvailable = data?.series && data.series.length > 0; 21 | return ( 22 |
23 | 24 | {dataAvailable ? ( 25 | 33 | {(config, alignedDataFrame) => { 34 | return ( 35 | <> 36 | 37 | 43 | 44 | ); 45 | }} 46 | 47 | ) : ( 48 |
49 |

No data to display.

50 |
51 | )} 52 |
53 | ); 54 | }; 55 | -------------------------------------------------------------------------------- /src/panels/troubleshooting/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "name": "PCP Troubleshooting Panel", 4 | "id": "performancecopilot-troubleshooting-panel", 5 | "info": { 6 | "description": "Grafana Panel for displaying time series graphs with troubleshooting information in PCP Checklist dashboards", 7 | "logos": { 8 | "small": "img/pcp-logo.svg", 9 | "large": "img/pcp-logo.svg" 10 | }, 11 | "links": [ 12 | { 13 | "name": "GitHub", 14 | "url": "https://github.com/performancecopilot/grafana-pcp" 15 | } 16 | ] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/panels/troubleshooting/styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'emotion'; 2 | import { GrafanaTheme } from '@grafana/data'; 3 | 4 | export const graphWrapper = css` 5 | position: relative; 6 | display: flex; 7 | flex-direction: column; 8 | height: 100%; 9 | `; 10 | 11 | export const buttons = css` 12 | position: absolute; 13 | right: 0; 14 | top: 0; 15 | z-index: 11; 16 | `; 17 | 18 | export const button = (theme: GrafanaTheme) => css` 19 | padding: 6px; 20 | box-sizing: content-box; 21 | border-radius: 50%; 22 | background: ${theme.colors.panelBg}; 23 | border: 0; 24 | 25 | & + & { 26 | margin-left: ${theme.spacing.sm}; 27 | } 28 | `; 29 | 30 | export const warningButton = (theme: GrafanaTheme) => css` 31 | color: ${theme.colors.formInputBorderInvalid}; 32 | 33 | &:hover { 34 | color: ${theme.colors.panelBg}; 35 | 36 | &:before { 37 | background: ${theme.colors.formInputBorderInvalid}; 38 | } 39 | } 40 | `; 41 | 42 | export const infoButton = (theme: GrafanaTheme) => css` 43 | color: ${theme.colors.formInputText}; 44 | 45 | &:hover { 46 | color: ${theme.colors.panelBg}; 47 | 48 | &:before { 49 | background: ${theme.colors.formInputText}; 50 | } 51 | } 52 | `; 53 | 54 | export const modalTypography = (theme: GrafanaTheme) => css` 55 | p:last-child { 56 | margin-bottom: 0; 57 | } 58 | 59 | ul, 60 | li { 61 | margin-left: ${theme.spacing.sm}; 62 | } 63 | 64 | a { 65 | color: ${theme.colors.linkExternal}; 66 | } 67 | `; 68 | 69 | export const modalArticleIcon = (theme: GrafanaTheme) => css` 70 | margin-right: ${theme.spacing.sm}; 71 | `; 72 | 73 | export const modalTooltipContent = (theme: GrafanaTheme) => css` 74 | border-bottom: 1px dotted ${theme.colors.textFaint}; 75 | `; 76 | 77 | export const modalRelativesLinksContainer = css` 78 | display: flex; 79 | width: 100%; 80 | justify-content: space-between; 81 | 82 | @media screen and (max-width: 992px) { 83 | flex-direction: column; 84 | } 85 | `; 86 | 87 | export const modalParentsLinks = css` 88 | margin-right: auto; 89 | `; 90 | 91 | export const modalChildrenLinks = css` 92 | margin-left: auto; 93 | `; 94 | 95 | export const notUsableContainer = (width: number, height: number) => css` 96 | display: flex; 97 | justify-content: center; 98 | align-items: center; 99 | width: ${width}px; 100 | height: ${height}px; 101 | text-align: center; 102 | `; 103 | -------------------------------------------------------------------------------- /src/panels/troubleshooting/types.ts: -------------------------------------------------------------------------------- 1 | import { VizLegendOptions, VizTooltipOptions } from '@grafana/ui'; 2 | 3 | export enum PredicateOperator { 4 | LesserThan = '<', 5 | GreaterThan = '>', 6 | } 7 | 8 | export interface Predicate { 9 | metric: string; 10 | operator: PredicateOperator; 11 | value: number; 12 | } 13 | 14 | export interface Metric { 15 | name: string; 16 | helptext: string; 17 | } 18 | 19 | export interface DerivedMetric { 20 | name: string; 21 | expr: string; 22 | } 23 | 24 | export interface LinkItem { 25 | title: string; 26 | name: string; 27 | uid: string; 28 | active?: boolean; 29 | } 30 | 31 | export interface TroubleshootingInfo { 32 | name: string; 33 | /** one line explanation of the issue */ 34 | warning?: string; 35 | /** a paragraph describing the cause and resolution */ 36 | description?: string; 37 | /** list of involved metrics */ 38 | metrics: Metric[]; 39 | /** list of derived metrics */ 40 | derivedMetrics?: DerivedMetric[]; 41 | /** test to determine if problem */ 42 | predicate?: Predicate; 43 | /** list of extern URLs (for example kbase articles) for addressing the issue */ 44 | urls?: string[]; 45 | /** notes describing any problems with the node (for example why predicate is broken) */ 46 | notes?: string; 47 | 48 | parents?: LinkItem[]; 49 | children?: LinkItem[]; 50 | } 51 | 52 | export interface Options { 53 | troubleshooting: TroubleshootingInfo; 54 | legend: VizLegendOptions; 55 | tooltipOptions: VizTooltipOptions; 56 | } 57 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/@grafana/toolkit/src/config/tsconfig.plugin.json", 3 | "include": ["src", "types"], 4 | "compilerOptions": { 5 | "rootDir": "./src", 6 | "baseUrl": "./src", 7 | "typeRoots": ["./node_modules/@types"], 8 | "lib": ["dom", "es2019", "esnext"], 9 | "resolveJsonModule": true 10 | } 11 | } 12 | --------------------------------------------------------------------------------