├── .bra.toml ├── .changeset ├── README.md ├── changelog.js └── config.json ├── .config ├── .cprc.json ├── .eslintrc ├── .prettierrc.js ├── Dockerfile ├── README.md ├── jest-setup.js ├── jest.config.js ├── jest │ ├── mocks │ │ └── react-inlinesvg.tsx │ └── utils.js ├── tsconfig.json ├── types │ └── custom.d.ts └── webpack │ ├── constants.ts │ ├── utils.ts │ └── webpack.config.ts ├── .eslintrc ├── .gitattributes ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── add-to-project.yml │ ├── compatibility-50.yml │ ├── compatibility-60.yml │ ├── compatibility-70.yml │ ├── compatibility-72.yml │ ├── is-compatible.yml │ ├── publish.yaml │ ├── push.yaml │ └── stale.yml ├── .gitignore ├── .nvmrc ├── .prettierrc.js ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── Magefile.go ├── Makefile ├── README.md ├── cspell.config.json ├── debug-backend.sh ├── devenv ├── README.md ├── dashboards.yaml ├── dashboards │ ├── template_linux_server.json │ └── zabbix_datasource_features.json ├── datasources.yaml ├── grafana.ini ├── nginx │ ├── .htpasswd │ └── default.conf ├── zabbix50 │ ├── bootstrap │ │ ├── Dockerfile │ │ ├── bootstrap_config.py │ │ └── zbx_export_hosts_5.xml │ └── docker-compose.yml ├── zabbix60 │ ├── bootstrap │ │ ├── Dockerfile │ │ ├── bootstrap_config.py │ │ └── zbx_export_hosts.xml │ └── docker-compose.yml ├── zabbix70 │ ├── bootstrap │ │ ├── Dockerfile │ │ ├── bootstrap_config.py │ │ ├── zbx_export_hosts.json │ │ └── zbx_export_hosts.xml │ └── docker-compose.yml ├── zabbix72 │ ├── bootstrap │ │ ├── Dockerfile │ │ ├── bootstrap_config.py │ │ ├── zbx_export_hosts.json │ │ └── zbx_export_hosts.xml │ └── docker-compose.yml └── zas-agent │ ├── .dockerignore │ ├── Dockerfile │ ├── conf │ ├── zas_scenario_backend.cfg │ ├── zas_scenario_database.cfg │ ├── zas_scenario_default.cfg │ └── zas_scenario_frontend.cfg │ ├── run_zas_agent.sh │ └── zas_scenario.cfg ├── docker-compose.yml ├── docs ├── Makefile ├── docs.mk ├── images │ ├── configuration-influxdb_ds_config.png │ ├── getstarting-dashboard_1.png │ ├── getstarting-metrics_filtering.png │ ├── getstarting-mysql_operations_1.png │ ├── getstarting-mysql_operations_2.png │ ├── getstarting-mysql_operations_3.png │ ├── getstarting-processor_load.png │ ├── getstarting-regex_backend_system_time.png │ ├── getstarting-regex_cpu_time.png │ ├── getstarting-singlestat_1.png │ ├── getstarting-singlestat_2.png │ ├── installation-add_datasource.png │ ├── installation-datasource_config.png │ ├── installation-enable_app.png │ ├── installation-mysql_ds_config.png │ ├── installation-plugin-dashboards.png │ ├── installation-plugins-apps.png │ ├── installation-postgres_ds_config.png │ ├── installation-test_connection.png │ ├── installation-test_connection_error.png │ ├── reference-direct-db-connection.svg │ ├── templating-menu.png │ ├── templating-query_with_variables.png │ └── templating-variable_editor.png ├── make-docs ├── sources │ ├── _index.md │ ├── configuration │ │ ├── _index.md │ │ ├── direct-db-datasource.md │ │ ├── provisioning.md │ │ └── troubleshooting.md │ ├── features.md │ ├── guides │ │ ├── _index.md │ │ └── templating.md │ ├── installation │ │ ├── _index.md │ │ ├── building-from-sources.md │ │ └── upgrade.md │ └── reference │ │ ├── _index.md │ │ ├── alerting.md │ │ ├── direct-db-connection.md │ │ └── functions.md └── variables.mk ├── go.mod ├── go.sum ├── jest-setup.js ├── jest.config.js ├── package.json ├── pkg ├── cache │ └── cache.go ├── datasource │ ├── datasource.go │ ├── datasource_test.go │ ├── functions.go │ ├── functions_test.go │ ├── models.go │ ├── resource_handler.go │ ├── response_handler.go │ ├── zabbix.go │ └── zabbix_test.go ├── gtime │ └── gtime.go ├── httpclient │ └── httpclient.go ├── metrics │ └── metrics.go ├── plugin.go ├── settings │ ├── models.go │ └── settings.go ├── timeseries │ ├── agg_functions.go │ ├── align.go │ ├── models.go │ ├── moving_average.go │ ├── sort.go │ ├── timeseries.go │ └── transform_functions.go ├── zabbix │ ├── cache.go │ ├── methods.go │ ├── models.go │ ├── testing.go │ ├── type_converters.go │ ├── utils.go │ ├── utils_test.go │ ├── zabbix.go │ └── zabbix_test.go └── zabbixapi │ ├── migration.go │ ├── testing.go │ ├── zabbix_api.go │ ├── zabbix_api_50_integration_test.go │ ├── zabbix_api_60_integration_test.go │ ├── zabbix_api_70_integration_test.go │ ├── zabbix_api_72_integration_test.go │ └── zabbix_api_test.go ├── playwright.config.ts ├── src ├── components │ ├── AckButton │ │ └── AckButton.tsx │ ├── ActionButton │ │ └── ActionButton.tsx │ ├── ConfigProvider │ │ └── ConfigProvider.tsx │ ├── ExecScriptButton │ │ └── ExecScriptButton.tsx │ ├── ExploreButton │ │ └── ExploreButton.tsx │ ├── FAIcon │ │ └── FAIcon.tsx │ ├── GFHeartIcon │ │ └── GFHeartIcon.tsx │ ├── MetricPicker │ │ ├── MetricPicker.tsx │ │ ├── MetricPickerMenu.tsx │ │ └── constants.ts │ ├── Modal │ │ └── ModalController.tsx │ └── index.ts ├── datasource │ ├── components │ │ ├── AnnotationQueryEditor.tsx │ │ ├── ConfigEditor.tsx │ │ ├── Divider.tsx │ │ ├── FunctionEditor │ │ │ ├── AddZabbixFunction.tsx │ │ │ ├── FunctionEditor.tsx │ │ │ ├── FunctionEditorControls.tsx │ │ │ ├── FunctionParamEditor.tsx │ │ │ ├── ZabbixFunctionEditor.tsx │ │ │ └── helpers.ts │ │ ├── QueryEditor.tsx │ │ ├── QueryEditor │ │ │ ├── ItemIdQueryEditor.tsx │ │ │ ├── MetricsQueryEditor.tsx │ │ │ ├── ProblemsQueryEditor.tsx │ │ │ ├── QueryEditorRow.tsx │ │ │ ├── QueryFunctionsEditor.tsx │ │ │ ├── QueryOptionsEditor.tsx │ │ │ ├── ServicesQueryEditor.tsx │ │ │ ├── TextMetricsQueryEditor.tsx │ │ │ ├── TriggersQueryEditor.tsx │ │ │ ├── UserMacrosQueryEditor.tsx │ │ │ └── utils.ts │ │ ├── VariableQueryEditor.tsx │ │ └── ZabbixInput.tsx │ ├── constants.ts │ ├── dashboards │ │ ├── template_linux_server.json │ │ ├── zabbix_server_dashboard.json │ │ └── zabbix_system_status.json │ ├── dataProcessor.ts │ ├── datasource.ts │ ├── img │ │ └── icn-zabbix-datasource.svg │ ├── metricFunctions.ts │ ├── migrations.ts │ ├── module.ts │ ├── plugin.json │ ├── problemsHandler.ts │ ├── query_help.md │ ├── responseHandler.spec.ts │ ├── responseHandler.ts │ ├── specs │ │ ├── datasource.spec.ts │ │ ├── dbConnector.test.ts │ │ ├── influxdbConnector.test.ts │ │ ├── migrations.test.ts │ │ ├── timeseries.spec.ts │ │ └── utils.spec.ts │ ├── timeseries.ts │ ├── tracking.ts │ ├── types.ts │ ├── types │ │ ├── config.ts │ │ └── query.ts │ ├── utils.ts │ └── zabbix │ │ ├── connectors │ │ ├── dbConnector.ts │ │ ├── influxdb │ │ │ └── influxdbConnector.ts │ │ ├── sql │ │ │ ├── mysql.ts │ │ │ ├── postgres.ts │ │ │ └── sqlConnector.ts │ │ └── zabbix_api │ │ │ ├── types.ts │ │ │ ├── zabbixAPIConnector.test.ts │ │ │ └── zabbixAPIConnector.ts │ │ ├── proxy │ │ └── cachingProxy.ts │ │ ├── types.ts │ │ ├── zabbix.test.ts │ │ └── zabbix.ts ├── img │ ├── icn-zabbix-app.svg │ ├── screenshot-annotations.png │ ├── screenshot-dashboard01.png │ ├── screenshot-metric_editor.png │ ├── screenshot-showcase.png │ └── screenshot-triggers.png ├── module.ts ├── panel-triggers │ ├── ProblemsPanel.tsx │ ├── components │ │ ├── AckModal.tsx │ │ ├── AlertList │ │ │ ├── AlertAcknowledges.tsx │ │ │ ├── AlertCard.tsx │ │ │ ├── AlertIcon.tsx │ │ │ └── AlertList.tsx │ │ ├── EventTag.tsx │ │ ├── ExecScriptModal.tsx │ │ ├── ProblemColorEditor.tsx │ │ ├── Problems │ │ │ ├── AckCell.tsx │ │ │ ├── AcknowledgesList.tsx │ │ │ ├── ProblemDetails.tsx │ │ │ ├── ProblemExpression.tsx │ │ │ ├── ProblemGroups.tsx │ │ │ ├── ProblemHosts.tsx │ │ │ ├── ProblemItems.tsx │ │ │ ├── ProblemStatusBar.tsx │ │ │ ├── ProblemTimeline.tsx │ │ │ └── Problems.tsx │ │ └── ResetColumnsEditor.tsx │ ├── img │ │ └── icn-zabbix-problems-panel.svg │ ├── migrations.ts │ ├── module.tsx │ ├── plugin.json │ ├── types.ts │ └── utils.ts ├── plugin.json ├── styles │ ├── _grafana_variables.dark.scss │ ├── _grafana_variables.light.scss │ ├── _panel-problems.scss │ ├── _panel-triggers.scss │ ├── _query_editor.scss │ ├── _react-table.scss │ ├── _variables.dark.scss │ ├── _variables.light.scss │ ├── _variables.scss │ ├── dark.scss │ ├── grafana-zabbix.scss │ └── light.scss └── test-setup │ ├── cssStub.js │ ├── jest-setup.js │ ├── mocks.ts │ ├── modules │ └── datemath.js │ └── panelStub.ts ├── tests └── e2e │ └── smoke.test.ts ├── tsconfig.json ├── webpack.config.ts └── yarn.lock /.bra.toml: -------------------------------------------------------------------------------- 1 | # default configuration created by the `mage watch` command. 2 | # this file can be edited and should be checked into source control. 3 | # see https://github.com/unknwon/bra/blob/master/templates/default.bra.toml for more configuration options. 4 | [run] 5 | init_cmds = [ 6 | ["mage", "-v", "build:backend"], 7 | ["mage", "-v" , "reloadPlugin"] 8 | ] 9 | watch_all = true 10 | follow_symlinks = false 11 | ignore = [".git", "node_modules", "dist"] 12 | ignore_files = ["mage_output_file.go"] 13 | watch_dirs = [ 14 | "pkg", 15 | "src", 16 | ] 17 | watch_exts = [".go", ".json"] 18 | build_delay = 2000 19 | cmds = [ 20 | ["mage", "-v", "build:backend"], 21 | ["mage", "-v" , "reloadPlugin"] 22 | ] -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/changelog.js: -------------------------------------------------------------------------------- 1 | const changelogFunctions = { 2 | getReleaseLine: async (changeset, type, options) => { 3 | let prefix = '🎉'; 4 | if (type === 'major') { 5 | prefix = '🎉'; 6 | } else if (type === 'minor') { 7 | prefix = '🚀'; 8 | } else if (type === 'patch') { 9 | prefix = '🐛'; 10 | } 11 | if (changeset && changeset.summary) { 12 | const summary = changeset.summary || ''; 13 | if (summary.indexOf('Docs') > -1) { 14 | prefix = '📝'; 15 | } 16 | if ( 17 | summary.indexOf('Chore') > -1 || 18 | summary.indexOf('grafana-plugin-sdk-go') > -1 || 19 | summary.indexOf('compiled') > -1 20 | ) { 21 | prefix = '⚙️'; 22 | } 23 | return [prefix, summary].join(' '); 24 | } 25 | return [prefix, changeset?.summary].join(' '); 26 | }, 27 | getDependencyReleaseLine: async (changesets, dependenciesUpdated, options) => { 28 | return '\n'; 29 | }, 30 | }; 31 | 32 | module.exports = changelogFunctions; 33 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@3.0.5/schema.json", 3 | "changelog": "./changelog.js", 4 | "commit": false, 5 | "fixed": [], 6 | "linked": [], 7 | "access": "restricted", 8 | "baseBranch": "main", 9 | "updateInternalDependencies": "patch", 10 | "ignore": [] 11 | } 12 | -------------------------------------------------------------------------------- /.config/.cprc.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "4.2.1" 3 | } 4 | -------------------------------------------------------------------------------- /.config/.eslintrc: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-eslint-config 6 | */ 7 | { 8 | "extends": ["@grafana/eslint-config"], 9 | "root": true, 10 | "rules": { 11 | "react/prop-types": "off" 12 | }, 13 | "overrides": [ 14 | { 15 | "plugins": ["deprecation"], 16 | "files": ["src/**/*.{ts,tsx}"], 17 | "rules": { 18 | "deprecation/deprecation": "warn" 19 | }, 20 | "parserOptions": { 21 | "project": "./tsconfig.json" 22 | } 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /.config/.prettierrc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in .config/README.md 5 | */ 6 | 7 | module.exports = { 8 | endOfLine: 'auto', 9 | printWidth: 120, 10 | trailingComma: 'es5', 11 | semi: true, 12 | jsxSingleQuote: false, 13 | singleQuote: true, 14 | useTabs: false, 15 | tabWidth: 2, 16 | }; 17 | -------------------------------------------------------------------------------- /.config/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG grafana_version=latest 2 | ARG grafana_image=grafana-enterprise 3 | 4 | FROM grafana/${grafana_image}:${grafana_version} 5 | 6 | # Make it as simple as possible to access the grafana instance for development purposes 7 | # Do NOT enable these settings in a public facing / production grafana instance 8 | ENV GF_AUTH_ANONYMOUS_ORG_ROLE "Admin" 9 | ENV GF_AUTH_ANONYMOUS_ENABLED "true" 10 | ENV GF_AUTH_BASIC_ENABLED "false" 11 | # Set development mode so plugins can be loaded without the need to sign 12 | ENV GF_DEFAULT_APP_MODE "development" 13 | 14 | # Inject livereload script into grafana index.html 15 | USER root 16 | RUN sed -i 's|||g' /usr/share/grafana/public/views/index.html 17 | -------------------------------------------------------------------------------- /.config/jest-setup.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config 6 | */ 7 | 8 | import '@testing-library/jest-dom'; 9 | 10 | // https://jestjs.io/docs/manual-mocks#mocking-methods-which-are-not-implemented-in-jsdom 11 | Object.defineProperty(global, 'matchMedia', { 12 | writable: true, 13 | value: jest.fn().mockImplementation((query) => ({ 14 | matches: false, 15 | media: query, 16 | onchange: null, 17 | addListener: jest.fn(), // deprecated 18 | removeListener: jest.fn(), // deprecated 19 | addEventListener: jest.fn(), 20 | removeEventListener: jest.fn(), 21 | dispatchEvent: jest.fn(), 22 | })), 23 | }); 24 | 25 | HTMLCanvasElement.prototype.getContext = () => {}; 26 | -------------------------------------------------------------------------------- /.config/jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-jest-config 6 | */ 7 | 8 | const path = require('path'); 9 | const { grafanaESModules, nodeModulesToTransform } = require('./jest/utils'); 10 | 11 | module.exports = { 12 | moduleNameMapper: { 13 | '\\.(css|scss|sass)$': 'identity-obj-proxy', 14 | 'react-inlinesvg': path.resolve(__dirname, 'jest', 'mocks', 'react-inlinesvg.tsx'), 15 | }, 16 | modulePaths: ['/src'], 17 | setupFilesAfterEnv: ['/jest-setup.js'], 18 | testEnvironment: 'jest-environment-jsdom', 19 | testMatch: [ 20 | '/src/**/__tests__/**/*.{js,jsx,ts,tsx}', 21 | '/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}', 22 | '/src/**/*.{spec,test,jest}.{js,jsx,ts,tsx}', 23 | ], 24 | transform: { 25 | '^.+\\.(t|j)sx?$': [ 26 | '@swc/jest', 27 | { 28 | sourceMaps: 'inline', 29 | jsc: { 30 | parser: { 31 | syntax: 'typescript', 32 | tsx: true, 33 | decorators: false, 34 | dynamicImport: true, 35 | }, 36 | }, 37 | }, 38 | ], 39 | }, 40 | // Jest will throw `Cannot use import statement outside module` if it tries to load an 41 | // ES module without it being transformed first. ./config/README.md#esm-errors-with-jest 42 | transformIgnorePatterns: [nodeModulesToTransform(grafanaESModules)], 43 | }; 44 | -------------------------------------------------------------------------------- /.config/jest/mocks/react-inlinesvg.tsx: -------------------------------------------------------------------------------- 1 | // Due to the grafana/ui Icon component making fetch requests to 2 | // `/public/img/icon/.svg` we need to mock react-inlinesvg to prevent 3 | // the failed fetch requests from displaying errors in console. 4 | 5 | import React from 'react'; 6 | 7 | type Callback = (...args: any[]) => void; 8 | 9 | export interface StorageItem { 10 | content: string; 11 | queue: Callback[]; 12 | status: string; 13 | } 14 | 15 | export const cacheStore: { [key: string]: StorageItem } = Object.create(null); 16 | 17 | const SVG_FILE_NAME_REGEX = /(.+)\/(.+)\.svg$/; 18 | 19 | const InlineSVG = ({ src }: { src: string }) => { 20 | // testId will be the file name without extension (e.g. `public/img/icons/angle-double-down.svg` -> `angle-double-down`) 21 | const testId = src.replace(SVG_FILE_NAME_REGEX, '$2'); 22 | return ; 23 | }; 24 | 25 | export default InlineSVG; 26 | -------------------------------------------------------------------------------- /.config/jest/utils.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in .config/README.md 5 | */ 6 | 7 | /* 8 | * This utility function is useful in combination with jest `transformIgnorePatterns` config 9 | * to transform specific packages (e.g.ES modules) in a projects node_modules folder. 10 | */ 11 | const nodeModulesToTransform = (moduleNames) => `node_modules\/(?!.*(${moduleNames.join('|')})\/.*)`; 12 | 13 | // Array of known nested grafana package dependencies that only bundle an ESM version 14 | const grafanaESModules = [ 15 | '.pnpm', // Support using pnpm symlinked packages 16 | '@grafana/schema', 17 | 'd3', 18 | 'd3-color', 19 | 'd3-force', 20 | 'd3-interpolate', 21 | 'd3-scale-chromatic', 22 | 'ol', 23 | 'react-colorful', 24 | 'rxjs', 25 | 'uuid', 26 | ]; 27 | 28 | module.exports = { 29 | nodeModulesToTransform, 30 | grafanaESModules, 31 | }; 32 | -------------------------------------------------------------------------------- /.config/tsconfig.json: -------------------------------------------------------------------------------- 1 | /* 2 | * ⚠️⚠️⚠️ THIS FILE WAS SCAFFOLDED BY `@grafana/create-plugin`. DO NOT EDIT THIS FILE DIRECTLY. ⚠️⚠️⚠️ 3 | * 4 | * In order to extend the configuration follow the steps in 5 | * https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/extend-configurations#extend-the-typescript-config 6 | */ 7 | { 8 | "compilerOptions": { 9 | "alwaysStrict": true, 10 | "declaration": false, 11 | "rootDir": "../src", 12 | "baseUrl": "../src", 13 | "typeRoots": ["../node_modules/@types"], 14 | "resolveJsonModule": true 15 | }, 16 | "ts-node": { 17 | "compilerOptions": { 18 | "module": "commonjs", 19 | "target": "es5", 20 | "esModuleInterop": true 21 | }, 22 | "transpileOnly": true 23 | }, 24 | "include": ["../src", "./types"], 25 | "extends": "@grafana/tsconfig" 26 | } 27 | -------------------------------------------------------------------------------- /.config/types/custom.d.ts: -------------------------------------------------------------------------------- 1 | // Image declarations 2 | declare module '*.gif' { 3 | const src: string; 4 | export default src; 5 | } 6 | 7 | declare module '*.jpg' { 8 | const src: string; 9 | export default src; 10 | } 11 | 12 | declare module '*.jpeg' { 13 | const src: string; 14 | export default src; 15 | } 16 | 17 | declare module '*.png' { 18 | const src: string; 19 | export default src; 20 | } 21 | 22 | declare module '*.webp' { 23 | const src: string; 24 | export default src; 25 | } 26 | 27 | declare module '*.svg' { 28 | const content: string; 29 | export default content; 30 | } 31 | 32 | // Font declarations 33 | declare module '*.woff'; 34 | declare module '*.woff2'; 35 | declare module '*.eot'; 36 | declare module '*.ttf'; 37 | declare module '*.otf'; 38 | -------------------------------------------------------------------------------- /.config/webpack/constants.ts: -------------------------------------------------------------------------------- 1 | export const SOURCE_DIR = 'src'; 2 | export const DIST_DIR = 'dist'; 3 | -------------------------------------------------------------------------------- /.config/webpack/utils.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import process from 'process'; 3 | import os from 'os'; 4 | import path from 'path'; 5 | import { glob } from 'glob'; 6 | import { SOURCE_DIR } from './constants'; 7 | 8 | export function isWSL() { 9 | if (process.platform !== 'linux') { 10 | return false; 11 | } 12 | 13 | if (os.release().toLowerCase().includes('microsoft')) { 14 | return true; 15 | } 16 | 17 | try { 18 | return fs.readFileSync('/proc/version', 'utf8').toLowerCase().includes('microsoft'); 19 | } catch { 20 | return false; 21 | } 22 | } 23 | 24 | export function getPackageJson() { 25 | return require(path.resolve(process.cwd(), 'package.json')); 26 | } 27 | 28 | export function getPluginJson() { 29 | return require(path.resolve(process.cwd(), `${SOURCE_DIR}/plugin.json`)); 30 | } 31 | 32 | export function hasReadme() { 33 | return fs.existsSync(path.resolve(process.cwd(), SOURCE_DIR, 'README.md')); 34 | } 35 | 36 | // Support bundling nested plugins by finding all plugin.json files in src directory 37 | // then checking for a sibling module.[jt]sx? file. 38 | export async function getEntries(): Promise> { 39 | const pluginsJson = await glob('**/src/**/plugin.json', { absolute: true }); 40 | 41 | const plugins = await Promise.all( 42 | pluginsJson.map((pluginJson) => { 43 | const folder = path.dirname(pluginJson); 44 | return glob(`${folder}/module.{ts,tsx,js,jsx}`, { absolute: true }); 45 | }) 46 | ); 47 | 48 | return plugins.reduce((result, modules) => { 49 | return modules.reduce((result, module) => { 50 | const pluginPath = path.dirname(module); 51 | const pluginName = path.relative(process.cwd(), pluginPath).replace(/src\/?/i, ''); 52 | const entryName = pluginName === '' ? 'module' : `${pluginName}/module`; 53 | 54 | result[entryName] = module; 55 | return result; 56 | }, result); 57 | }, {}); 58 | } 59 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.config/.eslintrc", 3 | "plugins": ["prettier"], 4 | "ignorePatterns": ["/src/test-setup/**/*"], 5 | "rules": { 6 | "react-hooks/exhaustive-deps": "off", 7 | "prettier/prettier": "error" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Don't diff files in dist/ 2 | *.map binary 3 | dist/** binary 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @grafana/oss-big-tent 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute to Grafana-Zabbix 2 | 3 | #### **Did you find a bug?** 4 | 5 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/alexanderzobnin/grafana-zabbix/issues). 6 | 7 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/alexanderzobnin/grafana-zabbix/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible. 8 | 9 | #### **Did you write a patch that fixes a bug?** 10 | 11 | * Open a new GitHub pull request with the patch. 12 | 13 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 14 | 15 | Thanks! :heart: :heart: :heart: 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **Expected behavior** 11 | A clear and concise description of what you expected to happen. 12 | 13 | **Screenshots** 14 | If applicable, add screenshots to help explain your problem. 15 | 16 | **Network data** 17 | If it's related to metric data visualization would be great to get the raw query and response for the network request (check this in browser dev tools network tab, there you can see metric requests, please include the request body and request response) 18 | 19 | **Software versions** 20 | 21 | | Grafana | Zabbix | Grafana-Zabbix Plugin | 22 | | ------- | ------ | --------------------- | 23 | | x.x.x | x.x.x | x.x.x | 24 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | allow: 8 | # Keep the sdk modules up-to-date 9 | - dependency-name: "github.com/grafana/grafana-plugin-sdk-go" 10 | dependency-type: "all" 11 | commit-message: 12 | prefix: "Upgrade grafana-plugin-sdk-go " 13 | include: "scope" 14 | reviewers: 15 | - "grafana/oss-big-tent" -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | * Link the PR to the related issue 2 | * If there's no issue for the PR, please, create it first 3 | * Rebase your PR if it gets out of sync with main 4 | * Ensure your PR does not include dist/ directory 5 | 6 | **REMOVE THE TEXT ABOVE BEFORE CREATING THE PULL REQUEST** 7 | -------------------------------------------------------------------------------- /.github/workflows/add-to-project.yml: -------------------------------------------------------------------------------- 1 | name: Add issues to OSS Big Tent team project 2 | on: 3 | issues: 4 | types: 5 | - opened 6 | pull_request: 7 | types: 8 | - opened 9 | 10 | permissions: 11 | contents: read 12 | id-token: write 13 | 14 | jobs: 15 | add-to-project: 16 | name: Add issue to project 17 | runs-on: ubuntu-latest 18 | steps: 19 | - id: get-secrets 20 | uses: grafana/shared-workflows/actions/get-vault-secrets@main # zizmor: ignore[unpinned-uses] 21 | with: 22 | repo_secrets: | 23 | GITHUB_APP_ID=grafana-oss-big-tent:app-id 24 | GITHUB_APP_PRIVATE_KEY=grafana-oss-big-tent:private-key 25 | - name: Generate a token 26 | id: generate-token 27 | uses: actions/create-github-app-token@v1 28 | with: 29 | app-id: ${{ env.GITHUB_APP_ID }} 30 | private-key: ${{ env.GITHUB_APP_PRIVATE_KEY }} 31 | owner: ${{ github.repository_owner }} 32 | - uses: actions/add-to-project@main 33 | with: 34 | project-url: https://github.com/orgs/grafana/projects/457 35 | github-token: ${{ steps.generate-token.outputs.token }} 36 | -------------------------------------------------------------------------------- /.github/workflows/compatibility-50.yml: -------------------------------------------------------------------------------- 1 | name: zabbix_50 2 | run-name: Compatibility with Zabbix 5.0 test 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | compatibility-check: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | 21 | - uses: actions/setup-go@v5.3.0 22 | 23 | - name: Start Zabbix environment 24 | run: | 25 | docker compose -f devenv/zabbix50/docker-compose.yml up -d 26 | # Wait for Zabbix to be ready 27 | sleep 30 28 | 29 | - name: Run integration tests 30 | env: 31 | INTEGRATION_TEST50: 'true' 32 | ZABBIX_URL: 'https://localhost/api_jsonrpc.php' 33 | ZABBIX_USER: 'Admin' 34 | ZABBIX_PASSWORD: 'zabbix' 35 | run: go test -v ./pkg/zabbixapi/... 36 | 37 | - name: Cleanup 38 | if: always() 39 | run: docker compose -f devenv/zabbix50/docker-compose.yml down -v 40 | -------------------------------------------------------------------------------- /.github/workflows/compatibility-60.yml: -------------------------------------------------------------------------------- 1 | name: zabbix_60 2 | run-name: Compatibility with Zabbix 6.0 test 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | compatibility-check: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | 21 | - uses: actions/setup-go@v5.3.0 22 | 23 | - name: Start Zabbix environment 24 | run: | 25 | docker compose -f devenv/zabbix60/docker-compose.yml up -d 26 | # Wait for Zabbix to be ready 27 | sleep 30 28 | 29 | - name: Run integration tests 30 | env: 31 | INTEGRATION_TEST60: 'true' 32 | ZABBIX_URL: 'https://localhost/api_jsonrpc.php' 33 | ZABBIX_USER: 'Admin' 34 | ZABBIX_PASSWORD: 'zabbix' 35 | run: go test -v ./pkg/zabbixapi/... 36 | 37 | - name: Cleanup 38 | if: always() 39 | run: docker compose -f devenv/zabbix60/docker-compose.yml down -v 40 | -------------------------------------------------------------------------------- /.github/workflows/compatibility-70.yml: -------------------------------------------------------------------------------- 1 | name: zabbix_70 2 | run-name: Compatibility with Zabbix 7.0 test 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | compatibility-check: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | 21 | - uses: actions/setup-go@v5.3.0 22 | 23 | - name: Start Zabbix environment 24 | run: | 25 | docker compose -f devenv/zabbix70/docker-compose.yml up -d 26 | # Wait for Zabbix to be ready 27 | sleep 30 28 | 29 | - name: Run integration tests 30 | env: 31 | INTEGRATION_TEST70: 'true' 32 | ZABBIX_URL: 'https://localhost/api_jsonrpc.php' 33 | ZABBIX_USER: 'Admin' 34 | ZABBIX_PASSWORD: 'zabbix' 35 | run: go test -v ./pkg/zabbixapi/... 36 | 37 | - name: Cleanup 38 | if: always() 39 | run: docker compose -f devenv/zabbix70/docker-compose.yml down -v 40 | -------------------------------------------------------------------------------- /.github/workflows/compatibility-72.yml: -------------------------------------------------------------------------------- 1 | name: zabbix_72 2 | run-name: Compatibility with Zabbix 7.2 test 3 | permissions: 4 | contents: read 5 | 6 | on: 7 | push: 8 | branches: [main] 9 | pull_request: 10 | branches: [main] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | compatibility-check: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | 21 | - uses: actions/setup-go@v5.3.0 22 | 23 | - name: Start Zabbix environment 24 | run: | 25 | docker compose -f devenv/zabbix72/docker-compose.yml up -d 26 | # Wait for Zabbix to be ready 27 | sleep 30 28 | 29 | - name: Run integration tests 30 | env: 31 | INTEGRATION_TEST72: 'true' 32 | ZABBIX_URL: 'http://localhost:8188/api_jsonrpc.php' 33 | ZABBIX_USER: 'Admin' 34 | ZABBIX_PASSWORD: 'zabbix' 35 | run: go test -v ./pkg/zabbixapi/... 36 | 37 | - name: Cleanup 38 | if: always() 39 | run: docker compose -f devenv/zabbix72/docker-compose.yml down -v 40 | -------------------------------------------------------------------------------- /.github/workflows/is-compatible.yml: -------------------------------------------------------------------------------- 1 | name: Latest Grafana API compatibility check 2 | on: [pull_request] 3 | permissions: {} 4 | 5 | jobs: 6 | compatibilitycheck: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | with: 11 | persist-credentials: false 12 | - uses: actions/setup-node@v3 13 | with: 14 | node-version-file: '.nvmrc' 15 | cache: 'yarn' 16 | - name: Install dependencies 17 | run: yarn install --immutable --prefer-offline 18 | - name: Build plugin 19 | run: yarn build 20 | - name: Compatibility check 21 | run: npx @grafana/levitate@latest is-compatible --path src/module.ts --target @grafana/data,@grafana/ui,@grafana/runtime 22 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Plugins - CD 2 | run-name: Deploy ${{ inputs.branch }} to ${{ inputs.environment }} by @${{ github.actor }} 3 | permissions: 4 | attestations: write 5 | contents: write 6 | id-token: write 7 | 8 | on: 9 | workflow_dispatch: 10 | inputs: 11 | branch: 12 | description: Branch to publish from. Can be used to deploy PRs to dev 13 | default: main 14 | environment: 15 | description: Environment to publish to 16 | required: true 17 | type: choice 18 | options: 19 | - 'dev' 20 | - 'ops' 21 | - 'prod' 22 | docs-only: 23 | description: Only publish docs, do not publish the plugin 24 | default: false 25 | type: boolean 26 | 27 | jobs: 28 | cd: 29 | name: CD 30 | uses: grafana/plugin-ci-workflows/.github/workflows/cd.yml@main 31 | with: 32 | go-version: '1.24' 33 | golangci-lint-version: '1.64.6' 34 | branch: ${{ github.event.inputs.branch }} 35 | environment: ${{ github.event.inputs.environment }} 36 | docs-only: ${{ fromJSON(github.event.inputs.docs-only) }} 37 | run-playwright: true 38 | -------------------------------------------------------------------------------- /.github/workflows/push.yaml: -------------------------------------------------------------------------------- 1 | name: Plugins - CI 2 | permissions: 3 | contents: read 4 | id-token: write 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | 12 | jobs: 13 | ci: 14 | name: CI 15 | uses: grafana/plugin-ci-workflows/.github/workflows/ci.yml@main 16 | with: 17 | go-version: '1.24' 18 | golangci-lint-version: '1.64.6' 19 | plugin-version-suffix: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }} 20 | run-playwright: true 21 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '30 1 * * *' 5 | 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | 10 | jobs: 11 | stale: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/stale@v9 15 | with: 16 | repo-token: ${{ secrets.GITHUB_TOKEN }} 17 | operations-per-run: 750 18 | # start from the oldest issues/PRs when performing stale operations 19 | ascending: true 20 | days-before-issue-stale: 730 21 | days-before-issue-close: 60 22 | stale-issue-label: stale 23 | exempt-issue-labels: no stalebot,type/epic,type/bug 24 | stale-issue-message: > 25 | This issue has been automatically marked as stale because it has not had 26 | activity in the last 2 years. It will be closed in 60 days if no further activity occurs. Please 27 | feel free to leave a comment if you believe the issue is still relevant. 28 | Thank you for your contributions! 29 | close-issue-message: > 30 | This issue has been automatically closed because it has not had any further 31 | activity in the last 60 days. Thank you for your contributions! 32 | days-before-pr-stale: 60 33 | days-before-pr-close: 14 34 | stale-pr-label: stale 35 | exempt-pr-labels: no stalebot 36 | stale-pr-message: > 37 | This pull request has been automatically marked as stale because it has not had 38 | activity in the last 60 days. It will be closed in 2 weeks if no further activity occurs. Please 39 | feel free to give a status update or ping for review. Thank you for your contributions! 40 | close-pr-message: > 41 | This pull request has been automatically closed because it has not had any further 42 | activity in the last 2 weeks. Thank you for your contributions! 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sublime-workspace 2 | *.sublime-project 3 | 4 | .idea/ 5 | .vscode 6 | 7 | *.bat 8 | .DS_Store 9 | 10 | # Grafana linter config 11 | # .jshintrc 12 | # .jscs.json 13 | # .jsfmtrc 14 | 15 | # Builded docs 16 | docs/site/ 17 | 18 | # Tests 19 | dist/test/ 20 | dist/test-setup/ 21 | 22 | vendor 23 | src/vendor 24 | src/vendor/npm 25 | node_modules 26 | coverage/ 27 | /tmp 28 | 29 | artifacts/ 30 | work/ 31 | 32 | # playwright 33 | test-results/ 34 | playwright-report/ 35 | blob-report/ 36 | playwright/.cache/ 37 | playwright/.auth/ 38 | 39 | # Tools logs 40 | npm-debug.log 41 | yarn-error.log 42 | 43 | # Built plugin 44 | dist/ 45 | ci/ 46 | alexanderzobnin-zabbix-app.zip 47 | 48 | .eslintcache 49 | 50 | # locally required config files 51 | public/css/*.min.css 52 | 53 | provisioning/ 54 | 55 | # SSL certificates 56 | devenv/nginx/nginx.crt 57 | devenv/nginx/nginx.key 58 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Prettier configuration provided by Grafana scaffolding 3 | ...require("./.config/.prettierrc.js") 4 | }; 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Plugin development 2 | 3 | ## Building plugin 4 | 5 | ```sh 6 | # install frontend deps 7 | yarn install --pure-lockfile 8 | # build frontend 9 | yarn build 10 | #build backend for current platform 11 | mage -v build:backend 12 | ``` 13 | 14 | ## Rebuild backend on changes 15 | 16 | ```sh 17 | mage watch 18 | ``` 19 | 20 | ## Debugging backend plugin 21 | 22 | For debugging backend part written on Go, you should go through a few steps. First, build a plugin with special flags for debugging: 23 | 24 | ```sh 25 | make build-debug 26 | ``` 27 | 28 | Then, configure your editor to connect to [delve](https://github.com/go-delve/delve) debugger running in headless mode. This is an example for VS Code: 29 | 30 | ```json 31 | { 32 | "version": "0.2.0", 33 | "configurations": [ 34 | { 35 | "name": "Debug backend plugin", 36 | "type": "go", 37 | "request": "attach", 38 | "mode": "remote", 39 | "port": 3222, 40 | "host": "127.0.0.1" 41 | } 42 | ] 43 | } 44 | ``` 45 | 46 | Finally, run grafana-server and then execute `./debug-backend.sh` from grafana-zabbix root folder. This script will attach delve to running plugin. Now you can go to the VS Code and run _Debug backend plugin_ debug config. 47 | 48 | ## Submitting PR 49 | 50 | If you are creating a PR, ensure to run `yarn changeset` from your branch. Provide the details accordingly. It will create `*.md` file inside `./.changeset` folder. Later during the release, based on these changesets, package version will be bumped and changelog will be generated. 51 | 52 | ## Releasing & Bumping version 53 | 54 | To create a new release, execute `yarn changeset version`. This will update the Changelog and bump the version in `package.json` file. Commit those changes. Run the `Plugins - CD` GitHub Action to publish the new release. 55 | -------------------------------------------------------------------------------- /Magefile.go: -------------------------------------------------------------------------------- 1 | //go:build mage 2 | // +build mage 3 | 4 | package main 5 | 6 | import ( 7 | // mage:import 8 | build "github.com/grafana/grafana-plugin-sdk-go/build" 9 | ) 10 | 11 | // Default configures the default target. 12 | var Default = build.BuildAll 13 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | all: install build test lint 2 | 3 | # Install dependencies 4 | install: 5 | # Frontend 6 | yarn install --pure-lockfile 7 | # Backend 8 | go install -v ./pkg/ 9 | go install golang.org/x/lint/golint@latest 10 | 11 | deps-go: 12 | go install -v ./pkg/ 13 | 14 | build: build-frontend build-backend 15 | build-frontend: 16 | yarn build 17 | 18 | build-backend: 19 | mage -v build:backend 20 | build-debug: 21 | mage -v build:debug 22 | 23 | run-frontend: 24 | yarn install --pure-lockfile 25 | yarn dev 26 | 27 | run-backend: 28 | # Rebuilds plugin on changes and kill running instance which forces grafana to restart plugin 29 | # See .bra.toml for bra configuration details 30 | bra run 31 | 32 | # Build plugin for all platforms (ready for distribution) 33 | dist: dist-frontend dist-backend 34 | dist-frontend: 35 | yarn build 36 | 37 | dist-backend: dist-backend-mage dist-backend-freebsd dist-arm-freebsd-arm64 38 | dist-backend-mage: 39 | mage -v buildAll 40 | dist-backend-windows: extension = .exe 41 | dist-backend-%: 42 | $(eval filename = gpx_zabbix-plugin_$*_amd64$(extension)) 43 | env CGO_ENABLED=0 GOOS=$* GOARCH=amd64 go build -ldflags="-s -w" -o ./dist/$(filename) ./pkg 44 | 45 | # ARM 46 | dist-arm: dist-arm-linux-arm-v6 dist-arm-linux-arm64 dist-arm-darwin-arm64 dist-arm-freebsd-arm64 47 | dist-arm-linux-arm-v6: 48 | env CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=6 go build -ldflags="-s -w" -o ./dist/gpx_zabbix-plugin_linux_arm ./pkg 49 | dist-arm-linux-arm-v7: 50 | env CGO_ENABLED=0 GOOS=linux GOARCH=arm GOARM=7 go build -ldflags="-s -w" -o ./dist/gpx_zabbix-plugin_linux_arm ./pkg 51 | dist-arm-%-arm64: 52 | $(eval filename = gpx_zabbix-plugin_$*_arm64$(extension)) 53 | env CGO_ENABLED=0 GOOS=$* GOARCH=arm64 go build -ldflags="-s -w" -o ./dist/$(filename) ./pkg 54 | 55 | .PHONY: test 56 | test: test-frontend test-backend 57 | test-frontend: 58 | yarn test 59 | test-backend: 60 | go test ./pkg/... 61 | test-ci: 62 | yarn ci-test 63 | mkdir -p tmp/coverage/golang/ 64 | go test -race -coverprofile=tmp/coverage/golang/coverage.txt -covermode=atomic ./pkg/... 65 | 66 | .PHONY: clean 67 | clean: 68 | -rm -r ./dist/ 69 | 70 | .PHONY: lint 71 | lint: 72 | yarn lint 73 | golint -min_confidence=1.1 -set_exit_status pkg/... 74 | 75 | sign-package: 76 | yarn sign 77 | 78 | package: install dist sign-package 79 | 80 | zip: 81 | cp -r dist/ alexanderzobnin-zabbix-app 82 | zip -r alexanderzobnin-zabbix-app.zip alexanderzobnin-zabbix-app 83 | rm -rf alexanderzobnin-zabbix-app 84 | -------------------------------------------------------------------------------- /cspell.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignorePaths": [ 3 | "coverage/**", 4 | "dist/**", 5 | "go.sum", 6 | "mage_output_file.go", 7 | "node_modules/**", 8 | "provisioning/**/*.yaml", 9 | "src/dashboards/*.json", 10 | "**/testdata/**/*.json", 11 | "**/testdata/**/*.jsonc", 12 | "vendor/**", 13 | "cspell.config.json", 14 | "package.json", 15 | "yarn.lock", 16 | "docker-compose*.yaml", 17 | "docker-compose*.yml", 18 | "src/test-setup/**", 19 | "src/styles/**", 20 | "webpack.config.ts", 21 | "ci/**", 22 | "devenv/**", 23 | "scripts/**", 24 | "docs/sources/css/**", 25 | "docs/src/css/**", 26 | "docs/**/*.yml", 27 | "docs/**/*.yaml", 28 | "dashboards/**", 29 | "src/**", 30 | "pkg/**" 31 | ], 32 | "ignoreRegExpList": ["import\\s*\\((.|[\r\n])*?\\)", "import\\s*.*\".*?\"", "\\[@.+\\]"], 33 | "words": [ 34 | "alexanderzobnin", 35 | "datapoint", 36 | "datapoints", 37 | "datasource", 38 | "datasources", 39 | "dompurify", 40 | "eventid", 41 | "eventids", 42 | "Gleb", 43 | "grafana", 44 | "groupid", 45 | "groupids", 46 | "hostid", 47 | "hostids", 48 | "iowait", 49 | "itemid", 50 | "itemids", 51 | "itemtype", 52 | "Ivanovsky", 53 | "lastchange", 54 | "lastvalue", 55 | "mydomain", 56 | "opdata", 57 | "percpu", 58 | "singlestat", 59 | "struct", 60 | "submatch", 61 | "templatig", 62 | "templating", 63 | "timeseries", 64 | "timeshift", 65 | "triggerid", 66 | "triggerids", 67 | "typeahead", 68 | "unmarshal", 69 | "uplot", 70 | "zabbix", 71 | "Zobnin" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /debug-backend.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [ "$1" == "-h" ]; then 3 | echo "Usage: ${BASH_SOURCE[0]} [plugin process name] [port]" 4 | exit 5 | fi 6 | 7 | PORT="${2:-3222}" 8 | PLUGIN_NAME="${1:-gpx_zabbix-plugin_}" 9 | 10 | # Build optimized for debug 11 | make build-debug 12 | 13 | # Reload plugin 14 | pkill ${PLUGIN_NAME} 15 | sleep 2 16 | 17 | if [ "$OSTYPE" == "linux-gnu" ]; then 18 | ptrace_scope=`cat /proc/sys/kernel/yama/ptrace_scope` 19 | if [ "$ptrace_scope" != 0 ]; then 20 | echo "WARNING: ptrace_scope set to value other than 0, this might prevent debugger from connecting, try writing \"0\" to /proc/sys/kernel/yama/ptrace_scope. 21 | Read more at https://www.kernel.org/doc/Documentation/security/Yama.txt" 22 | read -p "Set ptrace_scope to 0? y/N (default N)" set_ptrace_input 23 | if [ "$set_ptrace_input" == "y" ] || [ "$set_ptrace_input" == "Y" ]; then 24 | echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope 25 | fi 26 | fi 27 | fi 28 | 29 | PLUGIN_PID=`pgrep ${PLUGIN_NAME}` 30 | dlv attach ${PLUGIN_PID} --headless --listen=:${PORT} --api-version 2 --log 31 | pkill dlv 32 | -------------------------------------------------------------------------------- /devenv/README.md: -------------------------------------------------------------------------------- 1 | # Development environment for the plugin 2 | 3 | This docker environment contains preconfigured Zabbix instance with several monitored hosts and preconfigured Grafana with added data source and dashboards for testing. Environment uses plugin built 4 | from sources, so in order to start environment, run commands from plugin root directory: 5 | 6 | ```shell 7 | # Build plugin 8 | make dist 9 | 10 | # Test plugin with Zabbix 6.0 11 | cd devenv/zabbix60 12 | docker-compose up -d 13 | ``` 14 | 15 | Run bootstrap again in case of error: 16 | 17 | ```shell 18 | docker-compose up -d --build bootstrap 19 | ``` 20 | 21 | Grafana will be available at http://localhost:3001 (with default `admin:admin` credentials). 22 | 23 | If you want to edit sources, do it, rebuild plugin and then restart grafana container: 24 | 25 | ```shell 26 | docker-compose restart grafana 27 | ``` 28 | -------------------------------------------------------------------------------- /devenv/dashboards.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | providers: 4 | - name: 'Zabbix test dashboards' 5 | folder: 'zabbix' 6 | folderUid: '' 7 | type: file 8 | allowUiUpdates: false 9 | updateIntervalSeconds: 60 10 | options: 11 | path: /devenv/dashboards 12 | -------------------------------------------------------------------------------- /devenv/datasources.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: 1 2 | 3 | datasources: 4 | - name: zabbix 5 | type: alexanderzobnin-zabbix-datasource 6 | access: proxy 7 | url: http://zabbix-web:8080/api_jsonrpc.php 8 | isDefault: true 9 | jsonData: 10 | username: Admin 11 | trends: true 12 | trendsFrom: "7d" 13 | trendsRange: "4d" 14 | secureJsonData: 15 | password: zabbix 16 | -------------------------------------------------------------------------------- /devenv/grafana.ini: -------------------------------------------------------------------------------- 1 | app_mode = development 2 | 3 | [log] 4 | level = debug 5 | 6 | [plugins] 7 | enable_alpha = true 8 | 9 | [plugin.alexanderzobnin-zabbix-datasource] 10 | path = /grafana-zabbix 11 | -------------------------------------------------------------------------------- /devenv/nginx/.htpasswd: -------------------------------------------------------------------------------- 1 | admin:$apr1$hosO.9ex$D2pmswVRynvDoLE.bYs2C. 2 | -------------------------------------------------------------------------------- /devenv/nginx/default.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80 default; 3 | listen 443 ssl; 4 | root /var/www/html/; 5 | index index.php; 6 | 7 | server_name localhost 127.0.0.1; 8 | ssl_certificate /etc/nginx/ssl/nginx.crt; 9 | ssl_certificate_key /etc/nginx/ssl/nginx.key; 10 | ssl_protocols TLSv1.2 TLSv1.3; 11 | ssl_ciphers HIGH:!aNULL:!MD5; 12 | 13 | auth_basic "Zabbix HTTP Auth"; 14 | auth_basic_user_file /etc/nginx/.htpasswd; 15 | 16 | # Prevent caching of authentication 17 | add_header Cache-Control "no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0"; 18 | add_header Pragma "no-cache"; 19 | add_header Expires "0"; 20 | 21 | location / { 22 | proxy_pass http://localhost:8080/; 23 | proxy_set_header Host $http_host; 24 | proxy_set_header X-Real-IP $remote_addr; 25 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 26 | proxy_set_header X-Forwarded-Proto $scheme; 27 | proxy_buffering on; 28 | } 29 | } -------------------------------------------------------------------------------- /devenv/zabbix50/bootstrap/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3 2 | 3 | ENV ZBX_API_URL=http://zabbix-web 4 | ENV ZBX_API_USER="Admin" 5 | ENV ZBX_API_PASSWORD="zabbix" 6 | ENV ZBX_CONFIG="zbx_export_hosts_5.xml" 7 | ENV ZBX_BOOTSTRAP_SCRIPT="bootstrap_config.py" 8 | 9 | RUN pip install pyzabbix 10 | 11 | WORKDIR / 12 | 13 | ADD ./${ZBX_CONFIG} /${ZBX_CONFIG} 14 | ADD ./bootstrap_config.py /bootstrap_config.py 15 | # RUN cp ./${ZBX_CONFIG} /${ZBX_CONFIG} 16 | 17 | # Run bootstrap_config.py when the container launches 18 | CMD ["python", "/bootstrap_config.py"] 19 | -------------------------------------------------------------------------------- /devenv/zabbix50/bootstrap/bootstrap_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from time import sleep 3 | from pyzabbix import ZabbixAPI, ZabbixAPIException 4 | 5 | zabbix_url = os.environ['ZBX_API_URL'] 6 | zabbix_user = os.environ['ZBX_API_USER'] 7 | zabbix_password = os.environ['ZBX_API_PASSWORD'] 8 | print(zabbix_url, zabbix_user, zabbix_password) 9 | 10 | zapi = ZabbixAPI(zabbix_url, timeout=5) 11 | 12 | for i in range(10): 13 | print("Trying to connected to Zabbix API %s" % zabbix_url) 14 | try: 15 | zapi.login(zabbix_user, zabbix_password) 16 | print("Connected to Zabbix API Version %s" % zapi.api_version()) 17 | break 18 | except ZabbixAPIException as e: 19 | print(e) 20 | sleep(10) 21 | except: 22 | print("Waiting") 23 | sleep(10) 24 | 25 | 26 | zapi.timeout = 600 27 | 28 | config_path = os.environ['ZBX_CONFIG'] 29 | import_rules = { 30 | 'applications': { 31 | 'createMissing': True, 32 | }, 33 | 'discoveryRules': { 34 | 'createMissing': True, 35 | 'updateExisting': True 36 | }, 37 | 'graphs': { 38 | 'createMissing': True, 39 | 'updateExisting': True 40 | }, 41 | 'groups': { 42 | 'createMissing': True 43 | }, 44 | 'hosts': { 45 | 'createMissing': True, 46 | 'updateExisting': True 47 | }, 48 | 'images': { 49 | 'createMissing': True, 50 | 'updateExisting': True 51 | }, 52 | 'items': { 53 | 'createMissing': True, 54 | 'updateExisting': True 55 | }, 56 | 'maps': { 57 | 'createMissing': True, 58 | 'updateExisting': True 59 | }, 60 | # 'screens': { 61 | # 'createMissing': True, 62 | # 'updateExisting': True 63 | # }, 64 | 'templateLinkage': { 65 | 'createMissing': True, 66 | }, 67 | 'templates': { 68 | 'createMissing': True, 69 | 'updateExisting': True 70 | }, 71 | # 'templateScreens': { 72 | # 'createMissing': True, 73 | # 'updateExisting': True 74 | # }, 75 | 'triggers': { 76 | 'createMissing': True, 77 | 'updateExisting': True 78 | }, 79 | } 80 | 81 | print("Importing Zabbix config from %s" % config_path) 82 | with open(config_path, 'r') as f: 83 | config = f.read() 84 | 85 | try: 86 | # https://github.com/lukecyca/pyzabbix/issues/62 87 | import_result = zapi.confimport("xml", config, import_rules) 88 | print(import_result) 89 | except ZabbixAPIException as e: 90 | print(e) 91 | 92 | for h in zapi.host.get(output="extend"): 93 | print(h['name']) 94 | -------------------------------------------------------------------------------- /devenv/zabbix60/bootstrap/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | 3 | ENV ZBX_API_URL=http://zabbix-web:8080 4 | ENV ZBX_API_USER="Admin" 5 | ENV ZBX_API_PASSWORD="zabbix" 6 | ENV ZBX_CONFIG="zbx_export_hosts.xml" 7 | ENV ZBX_BOOTSTRAP_SCRIPT="bootstrap_config.py" 8 | 9 | RUN pip install pyzabbix 10 | 11 | ADD ./bootstrap_config.py /bootstrap_config.py 12 | ADD ${ZBX_CONFIG} /${ZBX_CONFIG} 13 | 14 | WORKDIR / 15 | 16 | # Run bootstrap_config.py when the container launches 17 | CMD ["python", "/bootstrap_config.py"] 18 | -------------------------------------------------------------------------------- /devenv/zabbix60/bootstrap/bootstrap_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from time import sleep 3 | from pyzabbix import ZabbixAPI, ZabbixAPIException 4 | 5 | zabbix_url = os.environ['ZBX_API_URL'] 6 | zabbix_user = os.environ['ZBX_API_USER'] 7 | zabbix_password = os.environ['ZBX_API_PASSWORD'] 8 | print(zabbix_url, zabbix_user, zabbix_password) 9 | 10 | zapi = ZabbixAPI(zabbix_url, timeout=5) 11 | 12 | for i in range(10): 13 | print("Trying to connected to Zabbix API %s" % zabbix_url) 14 | try: 15 | zapi.login(zabbix_user, zabbix_password) 16 | print("Connected to Zabbix API Version %s" % zapi.api_version()) 17 | break 18 | except ZabbixAPIException as e: 19 | print e 20 | sleep(5) 21 | except: 22 | print("Waiting") 23 | sleep(5) 24 | 25 | 26 | config_path = os.environ['ZBX_CONFIG'] 27 | import_rules = { 28 | 'discoveryRules': { 29 | 'createMissing': True, 30 | 'updateExisting': True 31 | }, 32 | 'graphs': { 33 | 'createMissing': True, 34 | 'updateExisting': True 35 | }, 36 | 'groups': { 37 | 'createMissing': True 38 | }, 39 | 'hosts': { 40 | 'createMissing': True, 41 | 'updateExisting': True 42 | }, 43 | 'images': { 44 | 'createMissing': True, 45 | 'updateExisting': True 46 | }, 47 | 'items': { 48 | 'createMissing': True, 49 | 'updateExisting': True 50 | }, 51 | 'maps': { 52 | 'createMissing': True, 53 | 'updateExisting': True 54 | }, 55 | 'templateLinkage': { 56 | 'createMissing': True, 57 | }, 58 | 'templates': { 59 | 'createMissing': True, 60 | 'updateExisting': True 61 | }, 62 | 'triggers': { 63 | 'createMissing': True, 64 | 'updateExisting': True 65 | }, 66 | } 67 | 68 | print("Importing Zabbix config from %s" % config_path) 69 | with open(config_path, 'r') as f: 70 | config = f.read() 71 | 72 | try: 73 | # https://github.com/lukecyca/pyzabbix/issues/62 74 | import_result = zapi.confimport("xml", config, import_rules) 75 | print(import_result) 76 | except ZabbixAPIException as e: 77 | print e 78 | 79 | for h in zapi.host.get(output="extend"): 80 | print(h['name']) 81 | -------------------------------------------------------------------------------- /devenv/zabbix70/bootstrap/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim-bullseye 2 | 3 | ENV ZBX_API_URL=http://zabbix-web:8080 4 | ENV ZBX_API_USER="Admin" 5 | ENV ZBX_API_PASSWORD="zabbix" 6 | ENV ZBX_CONFIG="zbx_export_hosts.json" 7 | ENV ZBX_BOOTSTRAP_SCRIPT="bootstrap_config.py" 8 | 9 | RUN pip install zabbix_utils 10 | 11 | ADD ./bootstrap_config.py /bootstrap_config.py 12 | ADD ${ZBX_CONFIG} /${ZBX_CONFIG} 13 | 14 | WORKDIR / 15 | 16 | # Run bootstrap_config.py when the container launches 17 | CMD ["python", "/bootstrap_config.py"] 18 | -------------------------------------------------------------------------------- /devenv/zabbix70/bootstrap/bootstrap_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from zabbix_utils import ZabbixAPI 3 | 4 | zabbix_url = os.environ['ZBX_API_URL'] 5 | zabbix_user = os.environ['ZBX_API_USER'] 6 | zabbix_password = os.environ['ZBX_API_PASSWORD'] 7 | print(zabbix_url, zabbix_user, zabbix_password) 8 | 9 | zapi = ZabbixAPI(zabbix_url) 10 | 11 | for i in range(10): 12 | try: 13 | zapi.login(user=zabbix_user, password=zabbix_password) 14 | print("Connected to Zabbix API Version %s" % zapi.api_version()) 15 | break 16 | except Exception as e: 17 | print(e) 18 | 19 | config_path = os.environ['ZBX_CONFIG'] 20 | import_rules = { 21 | 'discoveryRules': { 22 | 'createMissing': True, 23 | 'updateExisting': True 24 | }, 25 | 'graphs': { 26 | 'createMissing': True, 27 | 'updateExisting': True 28 | }, 29 | 'host_groups': { 30 | 'createMissing': True 31 | }, 32 | 'hosts': { 33 | 'createMissing': True, 34 | 'updateExisting': True 35 | }, 36 | 'images': { 37 | 'createMissing': True, 38 | 'updateExisting': True 39 | }, 40 | 'items': { 41 | 'createMissing': True, 42 | 'updateExisting': True 43 | }, 44 | 'maps': { 45 | 'createMissing': True, 46 | 'updateExisting': True 47 | }, 48 | 'templateLinkage': { 49 | 'createMissing': True, 50 | }, 51 | 'templates': { 52 | 'createMissing': True, 53 | 'updateExisting': True 54 | }, 55 | 'triggers': { 56 | 'createMissing': True, 57 | 'updateExisting': True 58 | }, 59 | } 60 | 61 | print("Importing Zabbix config from %s" % config_path) 62 | with open(config_path, 'r') as f: 63 | config = f.read() 64 | 65 | try: 66 | import_result = zapi.configuration.import_(source=config, format="json", rules=import_rules) 67 | if import_result == True: 68 | print("Zabbix config imported successfully") 69 | else: 70 | print("Failed to import Zabbix config") 71 | except Exception as e: 72 | print(e) 73 | 74 | for h in zapi.host.get(output="extend"): 75 | print(h['name']) 76 | -------------------------------------------------------------------------------- /devenv/zabbix72/bootstrap/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.12-slim-bullseye 2 | 3 | ENV ZBX_API_URL=http://zabbix-web:8080 4 | ENV ZBX_API_USER="Admin" 5 | ENV ZBX_API_PASSWORD="zabbix" 6 | ENV ZBX_CONFIG="zbx_export_hosts.json" 7 | ENV ZBX_BOOTSTRAP_SCRIPT="bootstrap_config.py" 8 | 9 | RUN pip install zabbix_utils 10 | 11 | ADD ./bootstrap_config.py /bootstrap_config.py 12 | ADD ${ZBX_CONFIG} /${ZBX_CONFIG} 13 | 14 | WORKDIR / 15 | 16 | # Run bootstrap_config.py when the container launches 17 | CMD ["python", "/bootstrap_config.py"] 18 | -------------------------------------------------------------------------------- /devenv/zabbix72/bootstrap/bootstrap_config.py: -------------------------------------------------------------------------------- 1 | import os 2 | from zabbix_utils import ZabbixAPI 3 | 4 | zabbix_url = os.environ['ZBX_API_URL'] 5 | zabbix_user = os.environ['ZBX_API_USER'] 6 | zabbix_password = os.environ['ZBX_API_PASSWORD'] 7 | print(zabbix_url, zabbix_user, zabbix_password) 8 | 9 | zapi = ZabbixAPI(zabbix_url) 10 | 11 | for i in range(10): 12 | try: 13 | zapi.login(user=zabbix_user, password=zabbix_password) 14 | print("Connected to Zabbix API Version %s" % zapi.api_version()) 15 | break 16 | except Exception as e: 17 | print(e) 18 | 19 | config_path = os.environ['ZBX_CONFIG'] 20 | import_rules = { 21 | 'discoveryRules': { 22 | 'createMissing': True, 23 | 'updateExisting': True 24 | }, 25 | 'graphs': { 26 | 'createMissing': True, 27 | 'updateExisting': True 28 | }, 29 | 'host_groups': { 30 | 'createMissing': True 31 | }, 32 | 'hosts': { 33 | 'createMissing': True, 34 | 'updateExisting': True 35 | }, 36 | 'images': { 37 | 'createMissing': True, 38 | 'updateExisting': True 39 | }, 40 | 'items': { 41 | 'createMissing': True, 42 | 'updateExisting': True 43 | }, 44 | 'maps': { 45 | 'createMissing': True, 46 | 'updateExisting': True 47 | }, 48 | 'templateLinkage': { 49 | 'createMissing': True, 50 | }, 51 | 'templates': { 52 | 'createMissing': True, 53 | 'updateExisting': True 54 | }, 55 | 'triggers': { 56 | 'createMissing': True, 57 | 'updateExisting': True 58 | }, 59 | } 60 | 61 | print("Importing Zabbix config from %s" % config_path) 62 | with open(config_path, 'r') as f: 63 | config = f.read() 64 | 65 | try: 66 | import_result = zapi.configuration.import_(source=config, format="json", rules=import_rules) 67 | if import_result == True: 68 | print("Zabbix config imported successfully") 69 | else: 70 | print("Failed to import Zabbix config") 71 | except Exception as e: 72 | print(e) 73 | 74 | for h in zapi.host.get(output="extend"): 75 | print(h['name']) 76 | -------------------------------------------------------------------------------- /devenv/zas-agent/.dockerignore: -------------------------------------------------------------------------------- 1 | # *.md 2 | # *.pdf 3 | Dockerfile 4 | Dockerfile* 5 | run_zas.sh 6 | */.git 7 | -------------------------------------------------------------------------------- /devenv/zas-agent/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:2.7 2 | 3 | # ENV ZAS_SOURCE_URL=https://github.com/vulogov/zas_agent/archive/master.zip 4 | # ENV ZAS_ARC_NAME=zas_agent-master 5 | # Use version with fixed redis dependency 6 | ENV ZAS_SOURCE_URL=https://github.com/alexanderzobnin/zas_agent/archive/refs/heads/redis-dependency.zip 7 | ENV ZAS_ARC_NAME=zas_agent-redis-dependency 8 | ENV ZAS_ARC_FILE=${ZAS_ARC_NAME}.zip 9 | ENV ZAS_WORKDIR="/zas-agent" 10 | 11 | RUN apt-get update && apt-get install -y ca-certificates 12 | RUN apt-get install -y unzip wget 13 | 14 | # Download and extract 15 | WORKDIR ${ZAS_WORKDIR} 16 | RUN wget ${ZAS_SOURCE_URL} -O ${ZAS_ARC_FILE} 17 | RUN unzip ${ZAS_ARC_FILE} 18 | 19 | # Install zas_agent 20 | WORKDIR ${ZAS_WORKDIR}/${ZAS_ARC_NAME}/install 21 | RUN python ./check_python_packages.py 22 | WORKDIR ${ZAS_WORKDIR}/${ZAS_ARC_NAME} 23 | RUN python setup.py build 24 | RUN python setup.py install 25 | 26 | COPY ./run_zas_agent.sh ${ZAS_WORKDIR}/${ZAS_ARC_NAME}/run_zas_agent.sh 27 | 28 | # Make port 10050 available to the world outside this container 29 | EXPOSE 10050 30 | 31 | # Set default redis port to connect 32 | ENV REDIS_PORT=6379 33 | ENV REDIS_HOST=redis 34 | 35 | # Run zas_agent.py when the container launches 36 | # ENTRYPOINT ["./run_zas_agent.sh"] 37 | CMD ["./run_zas_agent.sh"] 38 | -------------------------------------------------------------------------------- /devenv/zas-agent/conf/zas_scenario_backend.cfg: -------------------------------------------------------------------------------- 1 | ## 2 | ## This target will match agent.ping requests 3 | ## and return static value 1 4 | [agent.ping] 5 | value=static:1 6 | 7 | [agent.version] 8 | value=static:"zas-agent-master" 9 | 10 | [agent.hostname] 11 | value=cmd:uname,-n 12 | 13 | ## 14 | ## CPU time 15 | ## 16 | [system.cpu.util system] 17 | match=system.cpu.util\[,system\] 18 | value=scenario: 19 | scenario={"min":5,"max":30,"type":"float","variation_min":10,"variation_max":10, "spike_barrier":3,"spike_width":6} 20 | 21 | [system.cpu.util user] 22 | match=system.cpu.util\[,user\] 23 | value=scenario: 24 | scenario={"min":10,"max":40,"type":"float","variation_min":20,"variation_max":20, "spike_barrier":3,"spike_width":18} 25 | 26 | [system.cpu.util iowait] 27 | match=system.cpu.util\[,iowait\] 28 | value=scenario: 29 | scenario={"min":1,"max":30,"type":"float","variation_min":5,"variation_max":5, "spike_barrier":2,"spike_width":18} 30 | 31 | ## 32 | ## System load (1 min average) 33 | ## 34 | [system.cpu.load avg1] 35 | match=system.cpu.load\[percpu,avg1\] 36 | value=scenario: 37 | scenario={"min":0,"max":5,"type":"float","variation_min":20,"variation_max":20,"spike_barrier":5,"spike_width":6} 38 | 39 | [system.cpu.load avg15] 40 | match=system.cpu.load\[percpu,avg15\] 41 | value=scenario: 42 | scenario={"min":0.1,"max":3,"type":"float","variation_min":10,"variation_max":20, "spike_barrier":3,"spike_width":6} 43 | 44 | ## 45 | ## Network interfaces 46 | ## 47 | [all net.if.in] 48 | match=net.if.in* 49 | value=scenario: 50 | scenario={"min":10000000,"max":20000000,"type":"int","variation_rnd":1,"spike_barrier":3,"spike_width":6} 51 | [all net.if.out] 52 | match=net.if.out* 53 | value=scenario: 54 | scenario={"min":30000000,"max":50000000,"type":"int","variation_min":5,"variation_max":5,"spike_barrier":3,"spike_width":18} 55 | -------------------------------------------------------------------------------- /devenv/zas-agent/conf/zas_scenario_database.cfg: -------------------------------------------------------------------------------- 1 | ## 2 | ## This target will match agent.ping requests 3 | ## and return static value 1 4 | [agent.ping] 5 | value=static:1 6 | 7 | [agent.version] 8 | value=static:"zas-agent-master" 9 | 10 | [agent.hostname] 11 | value=cmd:uname,-n 12 | 13 | ## 14 | ## CPU time 15 | ## 16 | [system.cpu.util system] 17 | match=system.cpu.util\[,system\] 18 | value=scenario: 19 | scenario={"min":20,"max":30,"type":"float","variation_min":5,"variation_max":5, "spike_barrier":2,"spike_width":3} 20 | 21 | [system.cpu.util user] 22 | match=system.cpu.util\[,user\] 23 | value=scenario: 24 | scenario={"min":30,"max":50,"type":"float","variation_min":10,"variation_max":10, "spike_barrier":3,"spike_width":24} 25 | 26 | [system.cpu.util iowait] 27 | match=system.cpu.util\[,iowait\] 28 | value=scenario: 29 | scenario={"min":1,"max":20,"type":"float","variation_min":5,"variation_max":5, "spike_barrier":2,"spike_width":12} 30 | 31 | ## 32 | ## System load (1 min average) 33 | ## 34 | [system.cpu.load avg1] 35 | match=system.cpu.load\[percpu,avg1\] 36 | value=scenario: 37 | scenario={"min":1,"max":5,"type":"float","variation_min":20,"variation_max":20,"spike_barrier":5,"spike_width":6} 38 | 39 | [system.cpu.load avg15] 40 | match=system.cpu.load\[percpu,avg15\] 41 | value=scenario: 42 | scenario={"min":0.5,"max":3,"type":"float","variation_min":10,"variation_max":20, "spike_barrier":3,"spike_width":30} 43 | 44 | ## 45 | ## Network interfaces 46 | ## 47 | [all net.if.in] 48 | match=net.if.in* 49 | value=scenario: 50 | scenario={"min":60000000,"max":90000000,"type":"int","variation_rnd":1,"spike_barrier":2,"spike_width":3} 51 | [all net.if.out] 52 | match=net.if.out* 53 | value=scenario: 54 | scenario={"min":60000000,"max":90000000,"type":"int","variation_min":20,"variation_max":20,"spike_barrier":5,"spike_width":6} 55 | -------------------------------------------------------------------------------- /devenv/zas-agent/conf/zas_scenario_default.cfg: -------------------------------------------------------------------------------- 1 | ## 2 | ## This target will match all vfs.fs.size[*,pfree] requests 3 | ## and generate uniform random numbers in range 1 to 100 4 | ## 5 | [filesystems.size pfree] 6 | match=vfs.fs.size\[(.?),pfree\] 7 | value=uniform:1,100 8 | [application.connections] 9 | value=uniform_int:1,100 10 | 11 | ## 12 | ## This target will match all vfs.fs.size[*,free] requests 13 | ## and request data from REDIS 14 | ## 15 | [filesystem.size free] 16 | match=vfs.fs.size\[(.?),free\] 17 | value=redis: 18 | [all net.if.in] 19 | match=net.if.in* 20 | value=redis: 21 | [all net.if.out] 22 | match=net.if.out* 23 | value=redis: 24 | ## 25 | ## Scenario calculations 26 | ## 27 | [all system.cpu.util] 28 | match=system.cpu.util* 29 | value=scenario: 30 | scenario={"min":0,"max":20,"type":"float","variation_min":10,"variation_max":10} 31 | 32 | ## 33 | ## This target will match all vfs.fs.size[*,used] requests 34 | ## and request data from REDIS lists 35 | ## 36 | [filesystem.size used] 37 | match=vfs.fs.size\[(.?),used\] 38 | value=rqueue: 39 | 40 | ## 41 | ## This target will match agent.ping requests 42 | ## and return static value 1 43 | [agent.ping] 44 | value=static:1 45 | -------------------------------------------------------------------------------- /devenv/zas-agent/conf/zas_scenario_frontend.cfg: -------------------------------------------------------------------------------- 1 | ## 2 | ## This target will match agent.ping requests 3 | ## and return static value 1 4 | [agent.ping] 5 | value=static:1 6 | 7 | [agent.version] 8 | value=static:"zas-agent-master" 9 | 10 | [agent.hostname] 11 | value=cmd:uname,-n 12 | 13 | ## 14 | ## CPU time 15 | ## 16 | [system.cpu.util system] 17 | match=system.cpu.util\[,system\] 18 | value=scenario: 19 | scenario={"min":15,"max":20,"type":"float","variation_min":5,"variation_max":5, "spike_barrier":2,"spike_width":3} 20 | 21 | [system.cpu.util user] 22 | match=system.cpu.util\[,user\] 23 | value=scenario: 24 | scenario={"min":30,"max":50,"type":"float","variation_min":10,"variation_max":10, "spike_barrier":3,"spike_width":24} 25 | 26 | [system.cpu.util iowait] 27 | match=system.cpu.util\[,iowait\] 28 | value=scenario: 29 | scenario={"min":1,"max":30,"type":"float","variation_min":5,"variation_max":5, "spike_barrier":2,"spike_width":12} 30 | 31 | ## 32 | ## System load (1 min average) 33 | ## 34 | [system.cpu.load avg1] 35 | match=system.cpu.load\[percpu,avg1\] 36 | value=scenario: 37 | scenario={"min":0,"max":5,"type":"float","variation_min":10,"variation_max":30,"spike_barrier":5,"spike_width":30} 38 | 39 | [system.cpu.load avg15] 40 | match=system.cpu.load\[percpu,avg15\] 41 | value=scenario: 42 | scenario={"min":0.1,"max":3,"type":"float","variation_min":5,"variation_max":10, "spike_barrier":3,"spike_width":30} 43 | 44 | ## 45 | ## Network interfaces 46 | ## 47 | [all net.if.in] 48 | match=net.if.in* 49 | value=scenario: 50 | scenario={"min":5000000,"max":10000000,"type":"int","variation_rnd":1,"spike_barrier":3,"spike_width":20} 51 | [all net.if.out] 52 | match=net.if.out* 53 | value=scenario: 54 | scenario={"min":60000000,"max":90000000,"type":"int","variation_min":20,"variation_max":20,"spike_barrier":5,"spike_width":20} 55 | -------------------------------------------------------------------------------- /devenv/zas-agent/run_zas_agent.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Run redis server first 4 | # cd /zas/redis-3.2.9/src/ 5 | # nohup redis-server > /tmp/redis.log & 6 | 7 | REDIS_HOST=${REDIS_HOST:-"redis"} 8 | REDIS_PORT=${REDIS_PORT:-"6381"} 9 | 10 | rm -f /tmp/zas_agent.pid 11 | echo Using redis host: ${REDIS_HOST} and port: ${REDIS_PORT} 12 | zas_agent.py --start --user root --group root --scenario /etc/zas_scenario.cfg --redis_host ${REDIS_HOST} --redis_port ${REDIS_PORT} 13 | -------------------------------------------------------------------------------- /devenv/zas-agent/zas_scenario.cfg: -------------------------------------------------------------------------------- 1 | ## 2 | ## This target will match all vfs.fs.size[*,pfree] requests 3 | ## and generate uniform random numbers in range 1 to 100 4 | ## 5 | [filesystems.size pfree] 6 | match=vfs.fs.size\[(.?),pfree\] 7 | value=uniform:1,100 8 | [application.connections] 9 | value=uniform_int:1,100 10 | 11 | ## 12 | ## This target will match all vfs.fs.size[*,free] requests 13 | ## and request data from REDIS 14 | ## 15 | [filesystem.size free] 16 | match=vfs.fs.size\[(.?),free\] 17 | value=redis: 18 | [all net.if.in] 19 | match=net.if.in* 20 | value=scenario: 21 | scenario={"min":0,"max":100000,"type":"int","variation_rnd":1,"spike_barrier":3,"spike_width":6} 22 | [all net.if.out] 23 | match=net.if.out* 24 | value=scenario: 25 | scenario={"min":0,"max":100000,"type":"int","variation_min":5,"variation_max":5,"spike_barrier":10,"spike_width":6} 26 | ## 27 | ## Scenario calculations 28 | ## 29 | [all system.cpu.util] 30 | match=system.cpu.util* 31 | value=scenario: 32 | scenario={"min":0,"max":20,"type":"float","variation_min":10,"variation_max":10} 33 | 34 | [all system.cpu.load] 35 | match=system.cpu.load* 36 | value=scenario: 37 | scenario={"min":0,"max":5,"type":"float","variation_min":20,"variation_max":20} 38 | 39 | ## 40 | ## This target will match all vfs.fs.size[*,used] requests 41 | ## and request data from REDIS lists 42 | ## 43 | [filesystem.size used] 44 | match=vfs.fs.size\[(.?),used\] 45 | value=rqueue: 46 | 47 | ## 48 | ## This target will match agent.ping requests 49 | ## and return static value 1 50 | [agent.ping] 51 | value=static:1 52 | 53 | [agent.version] 54 | value=static:zas-0.1.1 55 | 56 | [agent.hostname] 57 | value=cmd:uname,-n 58 | 59 | [system.uname] 60 | value=cmd:uname,-a 61 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.0' 2 | 3 | services: 4 | grafana: 5 | container_name: 'grafana-zabbix' 6 | image: grafana/grafana-enterprise:${GF_VERSION:-main} 7 | ports: 8 | - 3000:3000/tcp 9 | volumes: 10 | - ./dist:/var/lib/grafana/plugins/grafana-zabbix 11 | - ./provisioning:/etc/grafana/provisioning 12 | environment: 13 | - TERM=linux 14 | - GF_DEFAULT_APP_MODE=development 15 | - GF_AUTH_ANONYMOUS_ENABLED=true 16 | - GF_AUTH_ANONYMOUS_ORG_ROLE=Admin 17 | - GF_ENTERPRISE_LICENSE_TEXT=$GF_ENTERPRISE_LICENSE_TEXT 18 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | .ONESHELL: 2 | .DELETE_ON_ERROR: 3 | export SHELL := bash 4 | export SHELLOPTS := pipefail:errexit 5 | MAKEFLAGS += --warn-undefined-variables 6 | MAKEFLAGS += --no-builtin-rule 7 | 8 | include docs.mk -------------------------------------------------------------------------------- /docs/images/configuration-influxdb_ds_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/configuration-influxdb_ds_config.png -------------------------------------------------------------------------------- /docs/images/getstarting-dashboard_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/getstarting-dashboard_1.png -------------------------------------------------------------------------------- /docs/images/getstarting-metrics_filtering.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/getstarting-metrics_filtering.png -------------------------------------------------------------------------------- /docs/images/getstarting-mysql_operations_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/getstarting-mysql_operations_1.png -------------------------------------------------------------------------------- /docs/images/getstarting-mysql_operations_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/getstarting-mysql_operations_2.png -------------------------------------------------------------------------------- /docs/images/getstarting-mysql_operations_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/getstarting-mysql_operations_3.png -------------------------------------------------------------------------------- /docs/images/getstarting-processor_load.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/getstarting-processor_load.png -------------------------------------------------------------------------------- /docs/images/getstarting-regex_backend_system_time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/getstarting-regex_backend_system_time.png -------------------------------------------------------------------------------- /docs/images/getstarting-regex_cpu_time.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/getstarting-regex_cpu_time.png -------------------------------------------------------------------------------- /docs/images/getstarting-singlestat_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/getstarting-singlestat_1.png -------------------------------------------------------------------------------- /docs/images/getstarting-singlestat_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/getstarting-singlestat_2.png -------------------------------------------------------------------------------- /docs/images/installation-add_datasource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/installation-add_datasource.png -------------------------------------------------------------------------------- /docs/images/installation-datasource_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/installation-datasource_config.png -------------------------------------------------------------------------------- /docs/images/installation-enable_app.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/installation-enable_app.png -------------------------------------------------------------------------------- /docs/images/installation-mysql_ds_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/installation-mysql_ds_config.png -------------------------------------------------------------------------------- /docs/images/installation-plugin-dashboards.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/installation-plugin-dashboards.png -------------------------------------------------------------------------------- /docs/images/installation-plugins-apps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/installation-plugins-apps.png -------------------------------------------------------------------------------- /docs/images/installation-postgres_ds_config.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/installation-postgres_ds_config.png -------------------------------------------------------------------------------- /docs/images/installation-test_connection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/installation-test_connection.png -------------------------------------------------------------------------------- /docs/images/installation-test_connection_error.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/installation-test_connection_error.png -------------------------------------------------------------------------------- /docs/images/templating-menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/templating-menu.png -------------------------------------------------------------------------------- /docs/images/templating-query_with_variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/templating-query_with_variables.png -------------------------------------------------------------------------------- /docs/images/templating-variable_editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/docs/images/templating-variable_editor.png -------------------------------------------------------------------------------- /docs/sources/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Grafana Zabbix plugin 3 | menuTitle: Grafana Zabbix plugin 4 | description: Introduction to Grafana-Zabbix plugin. 5 | aliases: 6 | keywords: 7 | - data source 8 | - zabbix 9 | labels: 10 | products: 11 | - oss 12 | - grafana cloud 13 | weight: 100 14 | --- 15 | 16 | # Grafana Zabbix plugin 17 | 18 | Grafana-Zabbix is a plugin for Grafana allowing to visualize monitoring data from Zabbix 19 | and create dashboards for analyzing metrics and realtime monitoring. Main goals of this project 20 | are extend Zabbix capabilities for monitoring data visualization and provide quick and powerful way 21 | to create dashboards. It is possible due both Grafana and Grafana-Zabbix plugin features. 22 | 23 | ## Community Resources, Feedback, and Support 24 | 25 | This project is being started as a simple plugin for Grafana. But many powerful features and 26 | improvements come from community. So don't hesitate to give any feedback and together we will make 27 | this tool better. 28 | 29 | If you have any troubles with Grafana or you just want clarification on a feature, there are 30 | a number of ways to get help: 31 | 32 | - [Troubleshooting guide](./configuration/troubleshooting/) 33 | - Search closed and open [issues on GitHub](https://github.com/grafana/grafana-zabbix/issues) 34 | - [Grafana Community](https://community.grafana.com) 35 | 36 | ## License 37 | 38 | By utilizing this software, you agree to the terms of the included license. Grafana-Zabbix plugin is 39 | licensed under the Apache 2.0 agreement. See 40 | [LICENSE](https://github.com/grafana/grafana-zabbix/blob/main/LICENSE) for the full 41 | license terms. 42 | -------------------------------------------------------------------------------- /docs/sources/configuration/direct-db-datasource.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Direct DB Data Source Configuration 3 | menuTitle: Direct DB Data Source Configuration 4 | description: Direct DB Data Source Configuration 5 | aliases: 6 | keywords: 7 | - data source 8 | - zabbix 9 | labels: 10 | products: 11 | - oss 12 | - grafana cloud 13 | weight: 310 14 | --- 15 | 16 | # Direct DB Data Source Configuration 17 | 18 | ## Security notes 19 | 20 | Grafana-Zabbix plugin can use MySQL, Postgres or InfluxDB (if Zabbix configured to store history data in InfluxDB) data sources to query history and trend data directly from Zabbix database. In order to execute queries, plugin needs only read access to the `history`, `history_uint`, `trends` and `trends_uint` tables. To make connection more secure and prevent unnecessary data disclosure, it's highly recommended to grant read access to only that tables. But if you want to use this data source for querying another data, you can 21 | grant `SELECT` privileges to entire zabbix database. Also, all queries are invoked by grafana server, so you can restrict connection to only grafana host. Here's MySQL example: 22 | 23 | ```sql 24 | GRANT SELECT ON zabbix.* TO 'grafana'@'grafana-host' identified by 'password'; 25 | ``` 26 | 27 | ## MySQL 28 | 29 | In order to use _Direct DB Connection_ feature you should configure SQL data source first. 30 | 31 | ![Configure MySQL data source](https://raw.githubusercontent.com/grafana/grafana-zabbix/main/docs/images/installation-mysql_ds_config.png) 32 | 33 | Select _MySQL_ data source type and provide your database host address and port (3306 is default for MySQL). Fill 34 | database name (usually, `zabbix`) and specify credentials. 35 | 36 | ## PostgreSQL 37 | 38 | Select _PostgreSQL_ data source type and provide your database host address and port (5432 is default). Fill 39 | database name (usually, `zabbix`) and specify credentials. 40 | 41 | ![Configure PostgreSQL data source](https://raw.githubusercontent.com/grafana/grafana-zabbix/main/docs/images/installation-postgres_ds_config.png) 42 | 43 | ## InfluxDB 44 | 45 | Select _InfluxDB_ data source type and provide your InfluxDB instance host address and port (8086 is default). Fill 46 | database name you configured in the [effluence](https://github.com/i-ky/effluence) module config (usually, `zabbix`) and specify credentials. 47 | 48 | ![Configure InfluxDB data source](https://raw.githubusercontent.com/grafana/grafana-zabbix/main/docs/images/configuration-influxdb_ds_config.png) 49 | -------------------------------------------------------------------------------- /docs/sources/configuration/provisioning.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Provisioning Grafana-Zabbix plugin 3 | menuTitle: Provisioning Grafana-Zabbix plugin 4 | description: Grafana-Zabbix plugin provisioning instructions. 5 | aliases: 6 | keywords: 7 | - data source 8 | - zabbix 9 | labels: 10 | products: 11 | - oss 12 | - grafana cloud 13 | weight: 320 14 | --- 15 | 16 | # Provisioning Grafana-Zabbix plugin 17 | 18 | It’s now possible to configure datasources using config files with Grafana’s provisioning system. You can read more about how it works and all the settings you can set for datasources on the [provisioning docs page](http://docs.grafana.org/administration/provisioning/#datasources) 19 | 20 | ## Example Datasource Config File 21 | 22 | ```yaml 23 | apiVersion: 1 24 | datasources: 25 | - name: Zabbix 26 | type: alexanderzobnin-zabbix-datasource 27 | url: http://localhost/zabbix/api_jsonrpc.php 28 | jsonData: 29 | # Zabbix API credentials 30 | username: zabbix 31 | # Trends options 32 | trends: true 33 | trendsFrom: '7d' 34 | trendsRange: '4d' 35 | # Cache update interval 36 | cacheTTL: '1h' 37 | # Alerting options 38 | alerting: true 39 | addThresholds: false 40 | alertingMinSeverity: 3 41 | # Direct DB Connection options 42 | dbConnectionEnable: true 43 | # Name of existing datasource for Direct DB Connection 44 | dbConnectionDatasourceName: MySQL Zabbix 45 | # Retention policy name (InfluxDB only) for fetching long-term stored data. 46 | # Leave it blank if only default retention policy used. 47 | dbConnectionRetentionPolicy: one_year 48 | # Disable acknowledges for read-only users 49 | disableReadOnlyUsersAck: true 50 | # Disable time series data alignment 51 | disableDataAlignment: false 52 | # Use value mapping from Zabbix 53 | useZabbixValueMapping: true 54 | secureJsonData: 55 | password: zabbix 56 | version: 1 57 | editable: false 58 | 59 | - name: MySQL Zabbix 60 | type: mysql 61 | url: localhost:3306 62 | user: grafana 63 | jsonData: 64 | database: zabbix 65 | secureJsonData: 66 | password: password 67 | 68 | - name: PostgreSQL Zabbix 69 | type: grafana-postgresql-datasource 70 | url: localhost:5432 71 | user: grafana 72 | jsonData: 73 | database: zabbix 74 | secureJsonData: 75 | password: password 76 | ``` 77 | 78 | For detailed provisioning configuration for mysql / postgres in direct db connection mode, refer [mysql plugin documentation](https://grafana.com/docs/grafana/latest/datasources/mysql/#provision-the-data-source) / [postgresql plugin documentation](https://grafana.com/docs/grafana/latest/datasources/postgres/#provision-the-data-source). 79 | -------------------------------------------------------------------------------- /docs/sources/configuration/troubleshooting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Troubleshooting 3 | menuTitle: Troubleshooting 4 | description: Troubleshooting guide for Grafana-Zabbix plugin. 5 | aliases: 6 | keywords: 7 | - data source 8 | - zabbix 9 | labels: 10 | products: 11 | - oss 12 | - grafana cloud 13 | weight: 330 14 | --- 15 | 16 | # Troubleshooting 17 | 18 | See [Grafana troubleshooting](http://docs.grafana.org/installation/troubleshooting/) for general 19 | connection issues. If you have a problem with Zabbix datasource, you should open 20 | a [support issue](https://github.com/alexanderzobnin/grafana-zabbix/issues). Before you do that 21 | please search the existing closed or open issues. 22 | -------------------------------------------------------------------------------- /docs/sources/features.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Feature Highlights 3 | menuTitle: Feature Highlights 4 | description: Grafana-Zabbix Feature Highlights. 5 | aliases: 6 | keywords: 7 | - data source 8 | - zabbix 9 | labels: 10 | products: 11 | - oss 12 | - grafana cloud 13 | weight: 110 14 | --- 15 | 16 | # Feature Highlights 17 | 18 | Grafana in couple with Grafana-Zabbix plugin allows to create great dashboards. There is some 19 | features: 20 | 21 | - Rich graphing features 22 | - Select multiple metrics [by using Regex](../guides/#multiple-items-on-one-graph) 23 | - Create interactive and reusable dashboards with [template variables](../guides/templating/) 24 | - Show events on graphs with [Annotations](http://docs.grafana.org/reference/annotations/) 25 | - Transform and shape your data with [metric processing functions](../reference/functions/) (Avg, Median, Min, Max, Multiply, Summarize, Time shift, Alias) 26 | - Mix metrics from multiple data sources in the same dashboard or panel 27 | - Create [alerts](../reference/alerting/) in Grafana 28 | - Display triggers with Problems panel 29 | - Discover and share [dashboards](https://grafana.com/dashboards) in the official library 30 | -------------------------------------------------------------------------------- /docs/sources/installation/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | menuTitle: Installation 4 | description: Installation instructions for Grafana-Zabbix. 5 | aliases: 6 | keywords: 7 | - data source 8 | - zabbix 9 | labels: 10 | products: 11 | - oss 12 | - grafana cloud 13 | weight: 200 14 | --- 15 | 16 | # Installation 17 | 18 | ## Choosing plugin version 19 | 20 | Currently (in version `4.x.x`) Grafana-Zabbix plugin supports Zabbix versions `4.x` and `5.x`. Zabbix `3.x` is not supported anymore. Generally, latest plugin should work with latest Grafana version, but if you have any compatibility issue, try to downgrade to previous minor release of Grafana. It's also helpful to report (but use search first to avoid duplicating issues) compatibility issues to the [GitHub](https://github.com/grafana/grafana-zabbix/issues). 21 | 22 | ## Using grafana-cli tool 23 | 24 | Get list of available plugins 25 | 26 | ```sh 27 | grafana-cli plugins list-remote 28 | ``` 29 | 30 | Install zabbix plugin 31 | 32 | ```sh 33 | grafana-cli plugins install alexanderzobnin-zabbix-app 34 | ``` 35 | 36 | Restart grafana after installing plugins 37 | 38 | ```sh 39 | systemctl restart grafana-server 40 | ``` 41 | 42 | Read more about installing plugins in [Grafana docs](https://grafana.com/docs/plugins/installation/) 43 | 44 | **WARNING!** The only reliable installation method is `grafana-cli`. Any other way should be treated as a workaround and doesn't provide any backward-compatibility guaranties. 45 | 46 | ## From github releases 47 | 48 | Starting from version 4.0, each plugin release on GitHub contains packaged plugin. To install it, go to [releases](https://github.com/grafana/grafana-zabbix/releases) page, pick release you want to get and click on `Assets`. Built plugin packaged into `zip` archive having name `alexanderzobnin-zabbix-app-x.x.x.zip`. Download it, unpack into your Grafana plugins directory and restart grafana server. Each plugin package contains [digital signature](https://grafana.com/docs/grafana/latest/plugins/plugin-signatures/) which allows Grafana to verify that plugin was published by it's owner and files are not modified. 49 | 50 | **Note**: `since` plugin version 4.0, `grafana-cli` downloads plugin from GitHub release. So downloading plugin package manually, you get the same package as via `grafana-cli`. 51 | 52 | ## Building from sources 53 | 54 | If you want to build a package yourself, or contribute - read [building instructions](./building-from-sources). 55 | -------------------------------------------------------------------------------- /docs/sources/installation/building-from-sources.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Building from sources 3 | menuTitle: Building from sources 4 | description: Building instructions for Grafana-Zabbix. 5 | aliases: 6 | keywords: 7 | - data source 8 | - zabbix 9 | labels: 10 | products: 11 | - oss 12 | - grafana cloud 13 | weight: 210 14 | --- 15 | 16 | # Building from sources 17 | 18 | If you want to build a package yourself, or contribute - here is a guide for how to do that. 19 | 20 | ## Prerequisites 21 | 22 | - [NodeJS](https://nodejs.org/) LTS 23 | - [Go](https://golang.org/) version 1.14 or above 24 | 25 | ## Building 26 | 27 | ### Install dependencies 28 | 29 | ```bash 30 | make install 31 | ``` 32 | 33 | ### Build plugin (for all platforms) 34 | 35 | ```bash 36 | make dist 37 | ``` 38 | 39 | ### To run frontend and rebuild on file change 40 | 41 | ```bash 42 | make run-frontend 43 | ``` 44 | 45 | ### To run backend and rebuild on file change 46 | 47 | ```bash 48 | make run-backend 49 | ``` 50 | 51 | ### Run tests 52 | 53 | ```bash 54 | make test 55 | ``` 56 | -------------------------------------------------------------------------------- /docs/sources/installation/upgrade.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Upgrade 3 | menuTitle: Upgrade 4 | description: Upgrade instructions for Grafana-Zabbix. 5 | aliases: 6 | keywords: 7 | - data source 8 | - zabbix 9 | labels: 10 | products: 11 | - oss 12 | - grafana cloud 13 | weight: 220 14 | --- 15 | 16 | # Upgrade 17 | 18 | ## Upgrade from 2.x 19 | 20 | After [enabling](../../configuration/#enable-plugin) Zabbix App go to _Data Sources_, open your configured Zabbix 21 | data source end select _Zabbix_ from _Type_ list again. This is needed because plugin id was changed 22 | in Grafana 3.0. 23 | -------------------------------------------------------------------------------- /docs/sources/reference/_index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: References 3 | menuTitle: References 4 | description: References 5 | aliases: 6 | keywords: 7 | - data source 8 | - zabbix 9 | labels: 10 | products: 11 | - oss 12 | - grafana cloud 13 | weight: 500 14 | --- 15 | 16 | # References 17 | 18 | - [Functions](./functions) 19 | - [Direct DB Connection](./direct-db-connection) 20 | - [Alerting](./alerting) -------------------------------------------------------------------------------- /docs/sources/reference/alerting.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Alerting 3 | menuTitle: Alerting 4 | description: Alerting 5 | aliases: 6 | keywords: 7 | - data source 8 | - zabbix 9 | labels: 10 | products: 11 | - oss 12 | - grafana cloud 13 | weight: 520 14 | --- 15 | 16 | ## Alerting overview 17 | 18 | Grafana-Zabbix plugin introduces [alerting](https://grafana.com/docs/grafana/latest/alerting/) feature support in 4.0 release. Work still in progress, so current alerting support has some limitations: 19 | 20 | - Only `Metrics` query mode supported. 21 | - Queries with data processing functions are not supported. 22 | 23 | ## Creating alerts 24 | 25 | In order to create alert, open panel query editor and switch to the `Alert` tab. Click `Create Alert` button, configure alert and save dashboard. Refer to [Grafana](https://grafana.com/docs/grafana/latest/alerting/create-alerts/) documentation for more details about alerts configuration. 26 | -------------------------------------------------------------------------------- /docs/variables.mk: -------------------------------------------------------------------------------- 1 | # List of projects to provide to the make-docs script. 2 | PROJECTS = plugins/alexanderzobnin-zabbix-app -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | // Jest setup provided by Grafana scaffolding 2 | import './.config/jest-setup'; 3 | import './src/test-setup/jest-setup'; 4 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // force timezone to UTC to allow tests to work regardless of local timezone 2 | // generally used by snapshots, but can affect specific tests 3 | process.env.TZ = 'UTC'; 4 | 5 | module.exports = { 6 | // Jest configuration provided by Grafana scaffolding 7 | ...require('./.config/jest.config'), 8 | }; 9 | -------------------------------------------------------------------------------- /pkg/cache/cache.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "time" 5 | 6 | cache "github.com/patrickmn/go-cache" 7 | ) 8 | 9 | const ( 10 | // For use with functions that take an expiration time. 11 | NoExpiration time.Duration = -1 12 | // For use with functions that take an expiration time. Equivalent to 13 | // passing in the same expiration duration as was given to New() or 14 | // NewFrom() when the cache was created (e.g. 5 minutes.) 15 | DefaultExpiration time.Duration = 0 16 | ) 17 | 18 | // TODO: since all methods moved to datasource_cache, this can be removed and replaced by go-cache 19 | 20 | // Cache is a abstraction over go-cache. 21 | type Cache struct { 22 | cache *cache.Cache 23 | } 24 | 25 | // NewCache creates a go-cache with expiration(ttl) time and cleanupInterval. 26 | func NewCache(ttl time.Duration, cleanupInterval time.Duration) *Cache { 27 | return &Cache{ 28 | cache.New(ttl, cleanupInterval), 29 | } 30 | } 31 | 32 | // Set the value of the key "request" to "rersponse" with default expiration time. 33 | func (c *Cache) Set(request string, response interface{}) { 34 | c.cache.SetDefault(request, response) 35 | } 36 | 37 | // Get the value associated with request from the cache 38 | func (c *Cache) Get(request string) (interface{}, bool) { 39 | return c.cache.Get(request) 40 | } 41 | -------------------------------------------------------------------------------- /pkg/datasource/datasource_test.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/grafana/grafana-plugin-sdk-go/backend" 8 | "gotest.tools/assert" 9 | ) 10 | 11 | func TestZabbixBackend_getCachedDatasource(t *testing.T) { 12 | basicDsSettings := backend.DataSourceInstanceSettings{ 13 | ID: 1, 14 | Name: "TestDatasource", 15 | URL: "http://zabbix.org/zabbix", 16 | JSONData: []byte("{}"), 17 | } 18 | 19 | modifiedDatasourceSettings := backend.DataSourceInstanceSettings{ 20 | ID: 1, 21 | Name: "TestDatasource", 22 | URL: "http://another.zabbix.org/zabbix", 23 | JSONData: []byte("{}"), 24 | } 25 | modifiedDatasource, _ := newZabbixDatasourceInstance(context.Background(), modifiedDatasourceSettings) 26 | 27 | basicDS, _ := newZabbixDatasourceInstance(context.Background(), basicDsSettings) 28 | 29 | tests := []struct { 30 | name string 31 | pluginContext backend.PluginContext 32 | want *ZabbixDatasourceInstance 33 | }{ 34 | { 35 | name: "Uncached Datasource (nothing in cache)", 36 | pluginContext: backend.PluginContext{ 37 | OrgID: 1, 38 | DataSourceInstanceSettings: &basicDsSettings, 39 | }, 40 | want: basicDS.(*ZabbixDatasourceInstance), 41 | }, 42 | { 43 | name: "Cached Datasource", 44 | pluginContext: backend.PluginContext{ 45 | OrgID: 1, 46 | DataSourceInstanceSettings: &basicDsSettings, 47 | }, 48 | want: basicDS.(*ZabbixDatasourceInstance), 49 | }, 50 | { 51 | name: "Cached then modified", 52 | pluginContext: backend.PluginContext{ 53 | OrgID: 1, 54 | DataSourceInstanceSettings: &modifiedDatasourceSettings, 55 | }, 56 | want: modifiedDatasource.(*ZabbixDatasourceInstance), 57 | }, 58 | } 59 | for _, tt := range tests { 60 | t.Run(tt.name, func(t *testing.T) { 61 | ds := NewZabbixDatasource() 62 | got, _ := ds.getDSInstance(context.Background(), tt.pluginContext) 63 | 64 | // Only checking the URL, being the easiest value to, and guarantee equality for 65 | assert.Equal(t, tt.want.zabbix.GetAPI().GetUrl().String(), got.zabbix.GetAPI().GetUrl().String()) 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /pkg/datasource/zabbix_test.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "github.com/alexanderzobnin/grafana-zabbix/pkg/settings" 5 | "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix" 6 | 7 | "github.com/grafana/grafana-plugin-sdk-go/backend" 8 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 9 | ) 10 | 11 | var basicDatasourceInfo = &backend.DataSourceInstanceSettings{ 12 | ID: 1, 13 | Name: "TestDatasource", 14 | URL: "http://zabbix.org/zabbix", 15 | JSONData: []byte(`{"username":"username", "password":"password", "cacheTTL":"10m", "authType":"token"}`), 16 | } 17 | 18 | func MockZabbixDataSource(body string, statusCode int) *ZabbixDatasourceInstance { 19 | zabbixSettings, _ := settings.ReadZabbixSettings(basicDatasourceInfo) 20 | zabbixClient, _ := zabbix.MockZabbixClient(basicDatasourceInfo, body, statusCode) 21 | 22 | return &ZabbixDatasourceInstance{ 23 | dsInfo: basicDatasourceInfo, 24 | zabbix: zabbixClient, 25 | Settings: zabbixSettings, 26 | logger: log.New(), 27 | } 28 | } 29 | 30 | func MockZabbixDataSourceResponse(dsInstance *ZabbixDatasourceInstance, body string, statusCode int) *ZabbixDatasourceInstance { 31 | zabbixClient, _ := zabbix.MockZabbixClientResponse(dsInstance.zabbix, body, statusCode) 32 | dsInstance.zabbix = zabbixClient 33 | 34 | return dsInstance 35 | } 36 | -------------------------------------------------------------------------------- /pkg/gtime/gtime.go: -------------------------------------------------------------------------------- 1 | package gtime 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "time" 8 | ) 9 | 10 | var dateUnitPattern = regexp.MustCompile(`^(\d+)([dwMy])$`) 11 | 12 | // ParseInterval parses an interval with support for all units that Grafana uses. 13 | func ParseInterval(interval string) (time.Duration, error) { 14 | result := dateUnitPattern.FindSubmatch([]byte(interval)) 15 | 16 | if len(result) != 3 { 17 | return time.ParseDuration(interval) 18 | } 19 | 20 | num, _ := strconv.Atoi(string(result[1])) 21 | period := string(result[2]) 22 | now := time.Now() 23 | 24 | switch period { 25 | case "d": 26 | return now.Sub(now.AddDate(0, 0, -num)), nil 27 | case "w": 28 | return now.Sub(now.AddDate(0, 0, -num*7)), nil 29 | case "M": 30 | return now.Sub(now.AddDate(0, -num, 0)), nil 31 | case "y": 32 | return now.Sub(now.AddDate(-num, 0, 0)), nil 33 | } 34 | 35 | return 0, fmt.Errorf("ParseInterval: invalid duration %q", interval) 36 | } 37 | -------------------------------------------------------------------------------- /pkg/httpclient/httpclient.go: -------------------------------------------------------------------------------- 1 | package httpclient 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "net/http" 7 | "time" 8 | 9 | simplejson "github.com/bitly/go-simplejson" 10 | 11 | "github.com/grafana/grafana-plugin-sdk-go/backend" 12 | "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" 13 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 14 | ) 15 | 16 | // New creates new HTTP client. 17 | func New(ctx context.Context, dsInfo *backend.DataSourceInstanceSettings, timeout time.Duration) (*http.Client, error) { 18 | clientOptions, err := dsInfo.HTTPClientOptions(ctx) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | clientOptions.Timeouts.Timeout = timeout 24 | 25 | tlsSkipVerify, err := getTLSSkipVerify(dsInfo) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | clientOptions.ConfigureTLSConfig = func(opts httpclient.Options, tlsConfig *tls.Config) { 31 | // grafana-plugin-sdk-go has a bug and InsecureSkipVerify only set if TLS Client Auth enabled, so it should be set 32 | // manually here 33 | tlsConfig.InsecureSkipVerify = tlsSkipVerify 34 | } 35 | 36 | client, err := httpclient.New(clientOptions) 37 | if err != nil { 38 | log.DefaultLogger.Error("Failed to create HTTP client", err) 39 | return nil, err 40 | } 41 | 42 | return client, nil 43 | } 44 | 45 | func getTLSSkipVerify(ds *backend.DataSourceInstanceSettings) (bool, error) { 46 | var tlsSkipVerify bool 47 | jsonData, err := simplejson.NewJson(ds.JSONData) 48 | if err != nil { 49 | return false, err 50 | } 51 | 52 | if jsonData != nil { 53 | tlsSkipVerify = jsonData.Get("tlsSkipVerify").MustBool(false) 54 | } 55 | 56 | return tlsSkipVerify, nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/metrics/metrics.go: -------------------------------------------------------------------------------- 1 | // This package contains internal plugin metrics exposed 2 | // by Grafana at /api/plugins/alexanderzobnin-zabbix-datasource/metrics 3 | // in Prometheus format 4 | 5 | package metrics 6 | 7 | import "github.com/prometheus/client_golang/prometheus" 8 | 9 | var ( 10 | // DataSourceQueryTotal is metric counter for getting total number of data source queries 11 | DataSourceQueryTotal *prometheus.CounterVec 12 | 13 | // ZabbixAPIQueryTotal is metric counter for getting total number of zabbix API queries 14 | ZabbixAPIQueryTotal *prometheus.CounterVec 15 | 16 | // CacheHitTotal is metric counter for getting total number of cache hits for requests 17 | CacheHitTotal *prometheus.CounterVec 18 | ) 19 | 20 | func init() { 21 | DataSourceQueryTotal = prometheus.NewCounterVec( 22 | prometheus.CounterOpts{ 23 | Name: "datasource_query_total", 24 | Help: "Total number of data source queries.", 25 | Namespace: "zabbix_datasource", 26 | }, 27 | []string{"query_type"}, 28 | ) 29 | 30 | ZabbixAPIQueryTotal = prometheus.NewCounterVec( 31 | prometheus.CounterOpts{ 32 | Name: "zabbix_api_query_total", 33 | Help: "Total number of Zabbix API queries.", 34 | Namespace: "zabbix_datasource", 35 | }, 36 | []string{"method"}, 37 | ) 38 | 39 | CacheHitTotal = prometheus.NewCounterVec( 40 | prometheus.CounterOpts{ 41 | Name: "cache_hit_total", 42 | Help: "Total number of cache hits.", 43 | Namespace: "zabbix_datasource", 44 | }, 45 | []string{"method"}, 46 | ) 47 | 48 | prometheus.MustRegister( 49 | DataSourceQueryTotal, 50 | ZabbixAPIQueryTotal, 51 | CacheHitTotal, 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /pkg/plugin.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/alexanderzobnin/grafana-zabbix/pkg/datasource" 7 | 8 | "github.com/grafana/grafana-plugin-sdk-go/backend" 9 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 10 | "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" 11 | ) 12 | 13 | const ZABBIX_PLUGIN_ID = "alexanderzobnin-zabbix-datasource" 14 | 15 | func main() { 16 | backend.SetupPluginEnvironment(ZABBIX_PLUGIN_ID) 17 | 18 | pluginLogger := log.New() 19 | mux := http.NewServeMux() 20 | ds := Init(pluginLogger, mux) 21 | httpResourceHandler := httpadapter.New(mux) 22 | 23 | pluginLogger.Debug("Starting Zabbix datasource") 24 | 25 | err := backend.Manage(ZABBIX_PLUGIN_ID, backend.ServeOpts{ 26 | CallResourceHandler: httpResourceHandler, 27 | QueryDataHandler: ds, 28 | CheckHealthHandler: ds, 29 | }) 30 | if err != nil { 31 | pluginLogger.Error("Error starting Zabbix datasource", "error", err.Error()) 32 | } 33 | } 34 | 35 | func Init(logger log.Logger, mux *http.ServeMux) *datasource.ZabbixDatasource { 36 | ds := datasource.NewZabbixDatasource() 37 | 38 | mux.HandleFunc("/", ds.RootHandler) 39 | mux.HandleFunc("/zabbix-api", ds.ZabbixAPIHandler) 40 | mux.HandleFunc("/db-connection-post", ds.DBConnectionPostProcessingHandler) 41 | 42 | return ds 43 | } 44 | -------------------------------------------------------------------------------- /pkg/settings/models.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import "time" 4 | 5 | const ( 6 | AuthTypeUserLogin = "userLogin" 7 | AuthTypeToken = "token" 8 | ) 9 | 10 | // ZabbixDatasourceSettingsDTO model 11 | type ZabbixDatasourceSettingsDTO struct { 12 | AuthType string `json:"authType"` 13 | Trends bool `json:"trends"` 14 | TrendsFrom string `json:"trendsFrom"` 15 | TrendsRange string `json:"trendsRange"` 16 | CacheTTL string `json:"cacheTTL"` 17 | Timeout interface{} `json:"timeout"` 18 | 19 | DisableDataAlignment bool `json:"disableDataAlignment"` 20 | DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"` 21 | } 22 | 23 | // ZabbixDatasourceSettings model 24 | type ZabbixDatasourceSettings struct { 25 | AuthType string 26 | Trends bool 27 | TrendsFrom time.Duration 28 | TrendsRange time.Duration 29 | CacheTTL time.Duration 30 | Timeout time.Duration 31 | 32 | DisableDataAlignment bool `json:"disableDataAlignment"` 33 | DisableReadOnlyUsersAck bool `json:"disableReadOnlyUsersAck"` 34 | } 35 | -------------------------------------------------------------------------------- /pkg/settings/settings.go: -------------------------------------------------------------------------------- 1 | package settings 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/alexanderzobnin/grafana-zabbix/pkg/gtime" 10 | 11 | "github.com/grafana/grafana-plugin-sdk-go/backend" 12 | ) 13 | 14 | func ReadZabbixSettings(dsInstanceSettings *backend.DataSourceInstanceSettings) (*ZabbixDatasourceSettings, error) { 15 | zabbixSettingsDTO := &ZabbixDatasourceSettingsDTO{} 16 | 17 | err := json.Unmarshal(dsInstanceSettings.JSONData, &zabbixSettingsDTO) 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | if zabbixSettingsDTO.AuthType == "" { 23 | zabbixSettingsDTO.AuthType = AuthTypeUserLogin 24 | } 25 | 26 | if zabbixSettingsDTO.TrendsFrom == "" { 27 | zabbixSettingsDTO.TrendsFrom = "7d" 28 | } 29 | if zabbixSettingsDTO.TrendsRange == "" { 30 | zabbixSettingsDTO.TrendsRange = "4d" 31 | } 32 | if zabbixSettingsDTO.CacheTTL == "" { 33 | zabbixSettingsDTO.CacheTTL = "1h" 34 | } 35 | 36 | //if zabbixSettingsDTO.Timeout == 0 { 37 | // zabbixSettingsDTO.Timeout = 30 38 | //} 39 | 40 | trendsFrom, err := gtime.ParseInterval(zabbixSettingsDTO.TrendsFrom) 41 | if err != nil { 42 | return nil, err 43 | } 44 | 45 | trendsRange, err := gtime.ParseInterval(zabbixSettingsDTO.TrendsRange) 46 | if err != nil { 47 | return nil, err 48 | } 49 | 50 | cacheTTL, err := gtime.ParseInterval(zabbixSettingsDTO.CacheTTL) 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | var timeout int64 56 | switch t := zabbixSettingsDTO.Timeout.(type) { 57 | case string: 58 | if t == "" { 59 | timeout = 30 60 | break 61 | } 62 | timeoutInt, err := strconv.Atoi(t) 63 | if err != nil { 64 | return nil, errors.New("failed to parse timeout: " + err.Error()) 65 | } 66 | timeout = int64(timeoutInt) 67 | case float64: 68 | timeout = int64(t) 69 | default: 70 | timeout = 30 71 | } 72 | 73 | zabbixSettings := &ZabbixDatasourceSettings{ 74 | AuthType: zabbixSettingsDTO.AuthType, 75 | Trends: zabbixSettingsDTO.Trends, 76 | TrendsFrom: trendsFrom, 77 | TrendsRange: trendsRange, 78 | CacheTTL: cacheTTL, 79 | Timeout: time.Duration(timeout) * time.Second, 80 | DisableDataAlignment: zabbixSettingsDTO.DisableDataAlignment, 81 | DisableReadOnlyUsersAck: zabbixSettingsDTO.DisableReadOnlyUsersAck, 82 | } 83 | 84 | return zabbixSettings, nil 85 | } 86 | -------------------------------------------------------------------------------- /pkg/timeseries/agg_functions.go: -------------------------------------------------------------------------------- 1 | package timeseries 2 | 3 | import ( 4 | "math" 5 | "sort" 6 | ) 7 | 8 | type AggregationFunc = func(points []TimePoint) *float64 9 | 10 | func AggAvg(points []TimePoint) *float64 { 11 | sum := AggSum(points) 12 | // Do not include nulls in the average 13 | nonNullLength := 0 14 | for _, p := range points { 15 | if p.Value != nil { 16 | nonNullLength++ 17 | } 18 | } 19 | if sum == nil || nonNullLength == 0 { 20 | return nil 21 | } 22 | avg := *sum / float64(nonNullLength) 23 | return &avg 24 | } 25 | 26 | func AggSum(points []TimePoint) *float64 { 27 | var sum float64 = 0 28 | isNil := true 29 | for _, p := range points { 30 | if p.Value != nil { 31 | isNil = false 32 | sum += *p.Value 33 | } 34 | } 35 | // Skip empty frames 36 | if isNil { 37 | return nil 38 | } 39 | return &sum 40 | } 41 | 42 | func AggMax(points []TimePoint) *float64 { 43 | var max *float64 = nil 44 | for _, p := range points { 45 | if p.Value != nil { 46 | if max == nil { 47 | max = p.Value 48 | } else if *p.Value > *max { 49 | max = p.Value 50 | } 51 | } 52 | } 53 | return max 54 | } 55 | 56 | func AggMin(points []TimePoint) *float64 { 57 | var min *float64 = nil 58 | for _, p := range points { 59 | if p.Value != nil { 60 | if min == nil { 61 | min = p.Value 62 | } else if *p.Value < *min { 63 | min = p.Value 64 | } 65 | } 66 | } 67 | return min 68 | } 69 | 70 | func AggCount(points []TimePoint) *float64 { 71 | count := float64(len(points)) 72 | return &count 73 | } 74 | 75 | func AggFirst(points []TimePoint) *float64 { 76 | return points[0].Value 77 | } 78 | 79 | func AggLast(points []TimePoint) *float64 { 80 | return points[len(points)-1].Value 81 | } 82 | 83 | func AggMedian(points []TimePoint) *float64 { 84 | return AggPercentile(50)(points) 85 | } 86 | 87 | func AggPercentile(n float64) AggregationFunc { 88 | return func(points []TimePoint) *float64 { 89 | values := make([]float64, 0) 90 | for _, p := range points { 91 | if p.Value != nil { 92 | values = append(values, *p.Value) 93 | } 94 | } 95 | if len(values) == 0 { 96 | return nil 97 | } 98 | 99 | sort.Float64s(values) 100 | percentileIndex := int(math.Floor(float64(len(values)) * n / 100)) 101 | percentile := values[percentileIndex] 102 | return &percentile 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /pkg/timeseries/models.go: -------------------------------------------------------------------------------- 1 | package timeseries 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbix" 8 | ) 9 | 10 | type TimePoint struct { 11 | Time time.Time 12 | Value *float64 13 | } 14 | 15 | func (p *TimePoint) UnmarshalJSON(data []byte) error { 16 | point := &struct { 17 | Time int64 18 | Value *float64 19 | }{} 20 | 21 | if err := json.Unmarshal(data, &point); err != nil { 22 | return err 23 | } 24 | 25 | p.Value = point.Value 26 | p.Time = time.Unix(point.Time, 0) 27 | 28 | return nil 29 | } 30 | 31 | type TimeSeries []TimePoint 32 | 33 | func NewTimeSeries() TimeSeries { 34 | return make(TimeSeries, 0) 35 | } 36 | 37 | func (ts *TimeSeries) Len() int { 38 | return len(*ts) 39 | } 40 | 41 | type TimeSeriesData struct { 42 | TS TimeSeries 43 | Meta TimeSeriesMeta 44 | } 45 | 46 | type TimeSeriesMeta struct { 47 | Name string 48 | Item *zabbix.Item 49 | 50 | // Item update interval. nil means not supported intervals (flexible, schedule, etc) 51 | Interval *time.Duration 52 | 53 | // AggValue is using for sorting purposes 54 | AggValue *float64 55 | } 56 | 57 | type AggFunc = func(points []TimePoint) *float64 58 | 59 | type TransformFunc = func(point TimePoint) TimePoint 60 | -------------------------------------------------------------------------------- /pkg/timeseries/sort.go: -------------------------------------------------------------------------------- 1 | package timeseries 2 | 3 | import ( 4 | "sort" 5 | "strconv" 6 | ) 7 | 8 | // SortBy sorts series by value calculated with provided aggFunc in given order 9 | func SortBy(series []*TimeSeriesData, order string, aggFunc AggFunc) []*TimeSeriesData { 10 | for _, s := range series { 11 | s.Meta.AggValue = aggFunc(s.TS) 12 | } 13 | 14 | // Sort by aggregated value 15 | sort.Slice(series, func(i, j int) bool { 16 | if series[i].Meta.AggValue != nil && series[j].Meta.AggValue != nil { 17 | return *series[i].Meta.AggValue < *series[j].Meta.AggValue 18 | } else if series[j].Meta.AggValue != nil { 19 | return true 20 | } 21 | return false 22 | }) 23 | 24 | if order == "desc" { 25 | reverseSeries := make([]*TimeSeriesData, len(series)) 26 | for i := 0; i < len(series); i++ { 27 | reverseSeries[i] = series[len(series)-1-i] 28 | } 29 | series = reverseSeries 30 | } 31 | 32 | return series 33 | } 34 | 35 | func SortByItem(series []*TimeSeriesData) []*TimeSeriesData { 36 | sort.Slice(series, func(i, j int) bool { 37 | itemIDi, err := strconv.Atoi(series[i].Meta.Item.ID) 38 | if err != nil { 39 | return false 40 | } 41 | 42 | itemIDj, err := strconv.Atoi(series[j].Meta.Item.ID) 43 | if err != nil { 44 | return false 45 | } 46 | 47 | return itemIDi < itemIDj 48 | }) 49 | 50 | return series 51 | } 52 | 53 | func SortByName(series []*TimeSeriesData, order string) []*TimeSeriesData { 54 | sort.Slice(series, func(i, j int) bool { 55 | if order == "desc" { 56 | return series[i].Meta.Name > series[j].Meta.Name 57 | } else { 58 | return series[i].Meta.Name < series[j].Meta.Name 59 | } 60 | }) 61 | 62 | return series 63 | } 64 | -------------------------------------------------------------------------------- /pkg/timeseries/transform_functions.go: -------------------------------------------------------------------------------- 1 | package timeseries 2 | 3 | import "time" 4 | 5 | func TransformScale(factor float64) TransformFunc { 6 | return func(point TimePoint) TimePoint { 7 | if point.Value != nil { 8 | newValue := *point.Value * factor 9 | point.Value = &newValue 10 | } 11 | return point 12 | } 13 | } 14 | 15 | func TransformOffset(offset float64) TransformFunc { 16 | return func(point TimePoint) TimePoint { 17 | if point.Value != nil { 18 | newValue := *point.Value + offset 19 | point.Value = &newValue 20 | } 21 | return point 22 | } 23 | } 24 | 25 | func TransformNull(nullValue float64) TransformFunc { 26 | return func(point TimePoint) TimePoint { 27 | if point.Value == nil { 28 | point.Value = &nullValue 29 | } 30 | return point 31 | } 32 | } 33 | 34 | func TransformRemoveAboveValue(threshold float64) TransformFunc { 35 | return func(point TimePoint) TimePoint { 36 | if point.Value != nil && *point.Value > threshold { 37 | point.Value = nil 38 | } 39 | return point 40 | } 41 | } 42 | 43 | func TransformRemoveBelowValue(threshold float64) TransformFunc { 44 | return func(point TimePoint) TimePoint { 45 | if point.Value != nil && *point.Value < threshold { 46 | point.Value = nil 47 | } 48 | return point 49 | } 50 | } 51 | 52 | func TransformShiftTime(interval time.Duration) TransformFunc { 53 | return func(point TimePoint) TimePoint { 54 | shiftedTime := point.Time.Add(interval) 55 | point.Time = shiftedTime 56 | return point 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pkg/zabbix/cache.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import ( 4 | "crypto/sha1" 5 | "encoding/hex" 6 | "time" 7 | 8 | "github.com/alexanderzobnin/grafana-zabbix/pkg/cache" 9 | ) 10 | 11 | var cachedMethods = map[string]bool{ 12 | "hostgroup.get": true, 13 | "host.get": true, 14 | "application.get": true, 15 | "item.get": true, 16 | "service.get": true, 17 | "usermacro.get": true, 18 | "proxy.get": true, 19 | "valuemap.get": true, 20 | } 21 | 22 | func IsCachedRequest(method string) bool { 23 | _, ok := cachedMethods[method] 24 | return ok 25 | } 26 | 27 | // ZabbixCache is a cache for datasource instance. 28 | type ZabbixCache struct { 29 | cache *cache.Cache 30 | } 31 | 32 | // NewZabbixCache creates a DatasourceCache with expiration(ttl) time and cleanupInterval. 33 | func NewZabbixCache(ttl time.Duration, cleanupInterval time.Duration) *ZabbixCache { 34 | return &ZabbixCache{ 35 | cache.NewCache(ttl, cleanupInterval), 36 | } 37 | } 38 | 39 | // GetAPIRequest gets request response from cache 40 | func (c *ZabbixCache) GetAPIRequest(request *ZabbixAPIRequest) (interface{}, bool) { 41 | requestHash := HashString(request.String()) 42 | return c.cache.Get(requestHash) 43 | } 44 | 45 | // SetAPIRequest writes request response to cache 46 | func (c *ZabbixCache) SetAPIRequest(request *ZabbixAPIRequest, response interface{}) { 47 | requestHash := HashString(request.String()) 48 | c.cache.Set(requestHash, response) 49 | } 50 | 51 | // HashString converts the given text string to hash string 52 | func HashString(text string) string { 53 | hash := sha1.New() 54 | hash.Write([]byte(text)) 55 | return hex.EncodeToString(hash.Sum(nil)) 56 | } 57 | -------------------------------------------------------------------------------- /pkg/zabbix/testing.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import ( 4 | "github.com/alexanderzobnin/grafana-zabbix/pkg/settings" 5 | "github.com/alexanderzobnin/grafana-zabbix/pkg/zabbixapi" 6 | "github.com/grafana/grafana-plugin-sdk-go/backend" 7 | "time" 8 | ) 9 | 10 | func MockZabbixClient(dsInfo *backend.DataSourceInstanceSettings, body string, statusCode int) (*Zabbix, error) { 11 | zabbixAPI, err := zabbixapi.MockZabbixAPI(body, statusCode) 12 | if err != nil { 13 | return nil, err 14 | } 15 | zabbixSettings := &settings.ZabbixDatasourceSettings{ 16 | Timeout: 10 * time.Second, 17 | } 18 | 19 | client, err := New(dsInfo, zabbixSettings, zabbixAPI) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | return client, nil 25 | } 26 | 27 | func MockZabbixClientResponse(client *Zabbix, body string, statusCode int) (*Zabbix, error) { 28 | zabbixAPI, err := zabbixapi.MockZabbixAPI(body, statusCode) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | client.api = zabbixAPI 34 | 35 | return client, nil 36 | } 37 | -------------------------------------------------------------------------------- /pkg/zabbix/type_converters.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/bitly/go-simplejson" 7 | "github.com/grafana/grafana-plugin-sdk-go/backend" 8 | ) 9 | 10 | func convertTo(value *simplejson.Json, result interface{}) error { 11 | valueJSON, err := value.MarshalJSON() 12 | if err != nil { 13 | return err 14 | } 15 | 16 | err = json.Unmarshal(valueJSON, result) 17 | if err != nil { 18 | backend.Logger.Debug("Error unmarshalling JSON", "error", err, "result", result) 19 | return err 20 | } 21 | 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /pkg/zabbix/utils.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strings" 7 | 8 | "github.com/dlclark/regexp2" 9 | "github.com/grafana/grafana-plugin-sdk-go/backend" 10 | ) 11 | 12 | func (item *Item) ExpandItemName() string { 13 | name := item.Name 14 | key := item.Key 15 | 16 | if !strings.Contains(key, "[") { 17 | return name 18 | } 19 | 20 | keyParamsStr := key[strings.Index(key, "[")+1 : strings.LastIndex(key, "]")] 21 | keyParams := splitKeyParams(keyParamsStr) 22 | 23 | for i := len(keyParams); i >= 1; i-- { 24 | name = strings.ReplaceAll(name, fmt.Sprintf("$%v", i), keyParams[i-1]) 25 | } 26 | 27 | return name 28 | } 29 | 30 | func expandItems(items []*Item) []*Item { 31 | for i := 0; i < len(items); i++ { 32 | items[i].Name = items[i].ExpandItemName() 33 | } 34 | return items 35 | } 36 | 37 | func splitKeyParams(paramStr string) []string { 38 | params := []string{} 39 | quoted := false 40 | inArray := false 41 | splitSymbol := "," 42 | param := "" 43 | 44 | for _, r := range paramStr { 45 | symbol := string(r) 46 | if symbol == `"` && inArray { 47 | param += symbol 48 | } else if symbol == `"` && quoted { 49 | quoted = false 50 | } else if symbol == `"` && !quoted { 51 | quoted = true 52 | } else if symbol == "[" && !quoted { 53 | inArray = true 54 | } else if symbol == "]" && !quoted { 55 | inArray = false 56 | } else if symbol == splitSymbol && !quoted && !inArray { 57 | params = append(params, param) 58 | param = "" 59 | } else { 60 | param += symbol 61 | } 62 | } 63 | 64 | params = append(params, param) 65 | return params 66 | } 67 | 68 | func parseFilter(filter string) (*regexp2.Regexp, error) { 69 | vaildREModifiers := "imncsxrde" 70 | regex := regexp.MustCompile(`^/(.+)/([imncsxrde]*)$`) 71 | flagRE := regexp.MustCompile(fmt.Sprintf("[%s]+", vaildREModifiers)) 72 | 73 | matches := regex.FindStringSubmatch(filter) 74 | if len(matches) <= 1 { 75 | return nil, nil 76 | } 77 | 78 | pattern := "" 79 | if matches[2] != "" { 80 | if flagRE.MatchString(matches[2]) { 81 | pattern += "(?" + matches[2] + ")" 82 | } else { 83 | return nil, backend.DownstreamError(fmt.Errorf("error parsing regexp: unsupported flags `%s` (expected [%s])", matches[2], vaildREModifiers)) 84 | } 85 | } 86 | pattern += matches[1] 87 | 88 | return regexp2.Compile(pattern, regexp2.RE2) 89 | } 90 | 91 | func isRegex(filter string) bool { 92 | regex := regexp.MustCompile(`^/(.+)/([imncsxrde]*)$`) 93 | return regex.MatchString(filter) 94 | } 95 | 96 | func itemTagToString(tag ItemTag) string { 97 | if tag.Value != "" { 98 | return fmt.Sprintf("%s: %s", tag.Tag, tag.Value) 99 | } else { 100 | return tag.Tag 101 | } 102 | } 103 | 104 | func parseItemTag(tagStr string) ItemTag { 105 | tag := ItemTag{} 106 | firstIdx := strings.Index(tagStr, ":") 107 | if firstIdx > 0 { 108 | tag.Tag = strings.TrimSpace(tagStr[:firstIdx]) 109 | if firstIdx < len(tagStr)-1 { 110 | tag.Value = strings.TrimSpace(tagStr[firstIdx+1:]) 111 | } 112 | } else { 113 | tag.Tag = strings.TrimSpace(tagStr) 114 | } 115 | return tag 116 | } 117 | -------------------------------------------------------------------------------- /pkg/zabbix/utils_test.go: -------------------------------------------------------------------------------- 1 | package zabbix 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/dlclark/regexp2" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestExpandItemName(t *testing.T) { 12 | tests := []struct { 13 | name string 14 | itemName string 15 | key string 16 | expected string 17 | }{ 18 | { 19 | name: "UNQUOTED_PARAMS", 20 | itemName: "CPU $2 time", 21 | key: "system.cpu.util[,user,avg1]", 22 | expected: "CPU user time", 23 | }, 24 | { 25 | name: "QUOTED_PARAMS_WITH_COMMAS", 26 | itemName: "CPU $1 $2 $3", 27 | key: "system.cpu.util[\"type=user,value=avg\",time,\"user\"]", 28 | expected: "CPU type=user,value=avg time user", 29 | }, 30 | { 31 | name: "MULTIPLE_ARRAY_PARAMS", 32 | itemName: "CPU $2 - $3 time", 33 | key: "system.cpu.util[,[user,system],avg1]", 34 | expected: "CPU user,system - avg1 time", 35 | }, 36 | { 37 | name: "MULTIPLE_ARRAY_PARAMS", 38 | itemName: "CPU - $2 - $3 - $4", 39 | key: "system.cpu.util[,[],[\"user,system\",iowait],avg1]", 40 | expected: "CPU - - \"user,system\",iowait - avg1", 41 | }, 42 | { 43 | name: "UNICODE_PARAMS", 44 | itemName: "CPU $1 $2 $3", 45 | key: "system.cpu.util[\"type=\b5Ὂg̀9! ℃ᾭG,value=avg\",time,\"user\"]", 46 | expected: "CPU type=\b5Ὂg̀9! ℃ᾭG,value=avg time user", 47 | }, 48 | } 49 | 50 | for _, tt := range tests { 51 | t.Run(tt.name, func(t *testing.T) { 52 | item := &Item{ 53 | Name: tt.itemName, 54 | Key: tt.key, 55 | } 56 | expandedName := item.ExpandItemName() 57 | assert.Equal(t, tt.expected, expandedName) 58 | }) 59 | } 60 | } 61 | 62 | func TestParseFilter(t *testing.T) { 63 | tests := []struct { 64 | name string 65 | filter string 66 | want *regexp2.Regexp 67 | expectNoError bool 68 | expectedError string 69 | }{ 70 | { 71 | name: "Simple regexp", 72 | filter: "/.*/", 73 | want: regexp2.MustCompile(".*", regexp2.RE2), 74 | expectNoError: true, 75 | expectedError: "", 76 | }, 77 | { 78 | name: "Not a regex", 79 | filter: "/var/lib/mysql: Total space", 80 | want: nil, 81 | expectNoError: true, 82 | expectedError: "", 83 | }, 84 | { 85 | name: "Regexp with modifier", 86 | filter: "/.*/i", 87 | want: regexp2.MustCompile("(?i).*", regexp2.RE2), 88 | expectNoError: true, 89 | expectedError: "", 90 | }, 91 | { 92 | name: "Regexp with unsupported modifier", 93 | filter: "/.*/1", 94 | want: nil, 95 | expectNoError: false, 96 | expectedError: "", 97 | }, 98 | } 99 | 100 | for _, tt := range tests { 101 | t.Run(tt.name, func(t *testing.T) { 102 | got, err := parseFilter(tt.filter) 103 | if tt.expectNoError { 104 | assert.NoError(t, err) 105 | } 106 | if tt.expectedError != "" { 107 | assert.Error(t, err) 108 | assert.EqualError(t, err, tt.expectedError) 109 | } 110 | if !reflect.DeepEqual(got, tt.want) { 111 | t.Errorf("parseFilter() = %v, want %v", got, tt.want) 112 | } 113 | }) 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /pkg/zabbixapi/migration.go: -------------------------------------------------------------------------------- 1 | package zabbixapi 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 7 | ) 8 | 9 | func normalizeParams(ctx context.Context, method string, params ZabbixAPIParams, version int) ZabbixAPIParams { 10 | logger := log.New().FromContext(ctx) 11 | logger.Debug("performing query params migration", "method", method, "version", version) 12 | switch method { 13 | case "trigger.get": 14 | newKey := "selectHostGroups" 15 | deprecatedKey := "selectGroups" 16 | deprecatedKeyValue, deprecatedKeyExists := params[deprecatedKey] 17 | newKeyValue, newKeyExists := params[newKey] 18 | if version < 70 && newKeyExists { 19 | if deprecatedKeyExists { 20 | delete(params, newKey) 21 | } 22 | if !deprecatedKeyExists { 23 | params[deprecatedKey] = newKeyValue 24 | delete(params, newKey) 25 | } 26 | } 27 | if version >= 70 && deprecatedKeyExists { 28 | if newKeyExists { 29 | delete(params, deprecatedKey) 30 | } 31 | if !newKeyExists { 32 | params[newKey] = deprecatedKeyValue 33 | delete(params, deprecatedKey) 34 | } 35 | } 36 | case "event.get": 37 | newKey := "selectAcknowledges" 38 | deprecatedKey := "select_acknowledges" 39 | deprecatedKeyValue, deprecatedKeyExists := params[deprecatedKey] 40 | newKeyValue, newKeyExists := params[newKey] 41 | if version < 70 && newKeyExists { 42 | if deprecatedKeyExists { 43 | delete(params, newKey) 44 | } 45 | if !deprecatedKeyExists { 46 | params[deprecatedKey] = newKeyValue 47 | delete(params, newKey) 48 | } 49 | } 50 | if version >= 70 && deprecatedKeyExists { 51 | if newKeyExists { 52 | delete(params, deprecatedKey) 53 | } 54 | if !newKeyExists { 55 | params[newKey] = deprecatedKeyValue 56 | delete(params, deprecatedKey) 57 | } 58 | } 59 | case "hostgroup.get": 60 | newKey := "with_hosts" 61 | deprecatedKey := "real_hosts" 62 | deprecatedKeyValue, deprecatedKeyExists := params[deprecatedKey] 63 | newKeyValue, newKeyExists := params[newKey] 64 | if version < 70 && newKeyExists { 65 | if deprecatedKeyExists { 66 | delete(params, newKey) 67 | } 68 | if !deprecatedKeyExists { 69 | params[deprecatedKey] = newKeyValue 70 | delete(params, newKey) 71 | } 72 | } 73 | if version >= 70 && deprecatedKeyExists { 74 | if newKeyExists { 75 | delete(params, deprecatedKey) 76 | } 77 | if !newKeyExists { 78 | params[newKey] = deprecatedKeyValue 79 | delete(params, deprecatedKey) 80 | } 81 | } 82 | } 83 | return params 84 | } 85 | -------------------------------------------------------------------------------- /pkg/zabbixapi/testing.go: -------------------------------------------------------------------------------- 1 | package zabbixapi 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | 9 | "github.com/grafana/grafana-plugin-sdk-go/backend/log" 10 | ) 11 | 12 | type RoundTripFunc func(req *http.Request) *http.Response 13 | 14 | func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { 15 | return f(req), nil 16 | } 17 | 18 | // NewTestClient returns *http.Client with Transport replaced to avoid making real calls 19 | func NewTestClient(fn RoundTripFunc) *http.Client { 20 | return &http.Client{ 21 | Transport: RoundTripFunc(fn), 22 | } 23 | } 24 | 25 | func MockZabbixAPI(body string, statusCode int) (*ZabbixAPI, error) { 26 | apiLogger := log.New() 27 | zabbixURL, err := url.Parse("http://zabbix.org/zabbix") 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return &ZabbixAPI{ 33 | url: zabbixURL, 34 | logger: apiLogger, 35 | 36 | httpClient: NewTestClient(func(req *http.Request) *http.Response { 37 | return &http.Response{ 38 | StatusCode: statusCode, 39 | Body: io.NopCloser(bytes.NewBufferString(body)), 40 | Header: make(http.Header), 41 | } 42 | }), 43 | }, nil 44 | } 45 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOptions } from '@grafana/plugin-e2e'; 2 | import { defineConfig, devices } from '@playwright/test'; 3 | import { dirname } from 'node:path'; 4 | 5 | const pluginE2eAuth = `${dirname(require.resolve('@grafana/plugin-e2e'))}/auth`; 6 | 7 | /** 8 | * Read environment variables from file. 9 | * https://github.com/motdotla/dotenv 10 | */ 11 | // require('dotenv').config(); 12 | 13 | /** 14 | * See https://playwright.dev/docs/test-configuration. 15 | */ 16 | export default defineConfig({ 17 | testDir: './tests/e2e', 18 | /* Run tests in files in parallel */ 19 | fullyParallel: true, 20 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 21 | forbidOnly: !!process.env.CI, 22 | /* Retry on CI only */ 23 | retries: process.env.CI ? 2 : 0, 24 | /* Opt out of parallel tests on CI. */ 25 | workers: process.env.CI ? 1 : undefined, 26 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 27 | reporter: 'html', 28 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 29 | use: { 30 | /* Base URL to use in actions like `await page.goto('/')`. */ 31 | baseURL: process.env.GRAFANA_URL || `http://localhost:${process.env.PORT || 3000}`, 32 | 33 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 34 | trace: 'on-first-retry', 35 | }, 36 | 37 | /* Configure projects for major browsers */ 38 | projects: [ 39 | // 1. Login to Grafana and store the cookie on disk for use in other tests. 40 | { 41 | name: 'auth', 42 | testDir: pluginE2eAuth, 43 | testMatch: [/.*\.js/], 44 | }, 45 | // 2. Run tests in Google Chrome. Every test will start authenticated as admin user. 46 | { 47 | name: 'chromium', 48 | use: { ...devices['Desktop Chrome'], storageState: 'playwright/.auth/admin.json' }, 49 | dependencies: ['auth'], 50 | }, 51 | ], 52 | }); 53 | -------------------------------------------------------------------------------- /src/components/AckButton/AckButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { ActionButton } from '../ActionButton/ActionButton'; 3 | 4 | interface Props { 5 | className?: string; 6 | onClick(): void; 7 | } 8 | 9 | export const AckButton: FC = ({ className, onClick }) => { 10 | return ; 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/ActionButton/ActionButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, PropsWithChildren } from 'react'; 2 | import { cx, css } from '@emotion/css'; 3 | import { stylesFactory, useTheme, Tooltip } from '@grafana/ui'; 4 | import { GrafanaTheme, GrafanaThemeType } from '@grafana/data'; 5 | import { FAIcon } from '../FAIcon/FAIcon'; 6 | 7 | interface Props { 8 | icon?: string; 9 | width?: number; 10 | tooltip?: string; 11 | className?: string; 12 | onClick(event: React.MouseEvent): void; 13 | } 14 | 15 | export const ActionButton: FC> = ({ icon, width, tooltip, className, children, onClick }) => { 16 | const theme = useTheme(); 17 | const styles = getStyles(theme); 18 | const buttonClass = cx( 19 | 'btn', 20 | styles.button, 21 | css` 22 | width: ${width || 3}rem; 23 | `, 24 | className 25 | ); 26 | 27 | let button = ( 28 | 32 | ); 33 | 34 | if (tooltip) { 35 | button = ( 36 | 37 | {button} 38 | 39 | ); 40 | } 41 | 42 | return button; 43 | }; 44 | 45 | const getStyles = stylesFactory((theme: GrafanaTheme) => { 46 | const actionBlue = theme.type === GrafanaThemeType.Light ? '#497dc0' : '#005f81'; 47 | const hoverBlue = theme.type === GrafanaThemeType.Light ? '#456ba4' : '#354f77'; 48 | 49 | return { 50 | button: css` 51 | height: 2rem; 52 | background-image: none; 53 | background-color: ${actionBlue}; 54 | border: 1px solid ${theme.palette.gray1}; 55 | border-radius: 1px; 56 | color: ${theme.colors.text}; 57 | 58 | i { 59 | vertical-align: middle; 60 | } 61 | 62 | &:hover { 63 | background-color: ${hoverBlue}; 64 | } 65 | `, 66 | icon: css` 67 | i { 68 | color: ${theme.colors.text}; 69 | } 70 | `, 71 | }; 72 | }); 73 | -------------------------------------------------------------------------------- /src/components/ConfigProvider/ConfigProvider.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { config, GrafanaBootConfig } from '@grafana/runtime'; 3 | import { ThemeContext } from '@grafana/ui'; 4 | import { createTheme } from '@grafana/data'; 5 | 6 | export const ConfigContext = React.createContext(config); 7 | export const ConfigConsumer = ConfigContext.Consumer; 8 | 9 | export const provideConfig = (component: React.ComponentType) => { 10 | const ConfigProvider = (props: any) => ( 11 | {React.createElement(component, { ...props })} 12 | ); 13 | 14 | return ConfigProvider; 15 | }; 16 | 17 | export const getCurrentTheme = () => 18 | createTheme({ 19 | colors: { 20 | mode: config.bootData.user.lightTheme ? 'light' : 'dark', 21 | }, 22 | }); 23 | 24 | export const ThemeProvider = ({ children }: { children: React.ReactNode }) => { 25 | return ( 26 | 27 | {(config) => { 28 | return {children}; 29 | }} 30 | 31 | ); 32 | }; 33 | 34 | export const provideTheme = (component: React.ComponentType) => { 35 | return provideConfig((props: any) => {React.createElement(component, { ...props })}); 36 | }; 37 | -------------------------------------------------------------------------------- /src/components/ExecScriptButton/ExecScriptButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { ActionButton } from '../ActionButton/ActionButton'; 3 | 4 | interface Props { 5 | className?: string; 6 | onClick(): void; 7 | } 8 | 9 | export const ExecScriptButton: FC = ({ className, onClick }) => { 10 | return ; 11 | }; 12 | -------------------------------------------------------------------------------- /src/components/ExploreButton/ExploreButton.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { locationService } from '@grafana/runtime'; 3 | import { ExploreUrlState, TimeRange, urlUtil } from '@grafana/data'; 4 | import { MODE_ITEMID, MODE_METRICS } from '../../datasource/constants'; 5 | import { ActionButton } from '../ActionButton/ActionButton'; 6 | import { expandItemName } from '../../datasource/utils'; 7 | import { ProblemDTO } from '../../datasource/types'; 8 | 9 | interface Props { 10 | problem: ProblemDTO; 11 | range: TimeRange; 12 | panelId: number; 13 | } 14 | 15 | export const ExploreButton: FC = ({ problem, panelId, range }) => { 16 | return ( 17 | openInExplore(problem, panelId, range)}> 18 | Explore 19 | 20 | ); 21 | }; 22 | 23 | const openInExplore = (problem: ProblemDTO, panelId: number, range: TimeRange) => { 24 | let query: any = {}; 25 | 26 | if (problem.items?.length === 1 && problem.hosts?.length === 1) { 27 | const item = problem.items[0]; 28 | const host = problem.hosts[0]; 29 | const itemName = expandItemName(item.name, item.key_); 30 | query = { 31 | queryType: MODE_METRICS, 32 | group: { filter: '/.*/' }, 33 | application: { filter: '' }, 34 | host: { filter: host.name }, 35 | item: { filter: itemName }, 36 | }; 37 | } else { 38 | const itemids = problem.items?.map((p) => p.itemid).join(','); 39 | query = { 40 | queryType: MODE_ITEMID, 41 | itemids: itemids, 42 | }; 43 | } 44 | 45 | const state: ExploreUrlState | any = { 46 | datasource: problem.datasource, 47 | context: 'explore', 48 | originPanelId: panelId, 49 | range: range.raw, 50 | queries: [query], 51 | }; 52 | 53 | const exploreState = JSON.stringify(state); 54 | const url = urlUtil.renderUrl('/explore', { left: exploreState }); 55 | locationService.push(url); 56 | }; 57 | -------------------------------------------------------------------------------- /src/components/FAIcon/FAIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { cx } from '@emotion/css'; 3 | 4 | interface Props { 5 | icon: string; 6 | customClass?: string; 7 | } 8 | 9 | export const FAIcon: FC = ({ icon, customClass }) => { 10 | const wrapperClass = cx(customClass, 'fa-icon-container'); 11 | 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export default FAIcon; 20 | -------------------------------------------------------------------------------- /src/components/GFHeartIcon/GFHeartIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { cx } from '@emotion/css'; 3 | 4 | interface Props { 5 | status: 'critical' | 'warning' | 'online' | 'ok' | 'problem'; 6 | className?: string; 7 | } 8 | 9 | export const GFHeartIcon: FC = ({ status, className }) => { 10 | const iconClass = cx( 11 | className, 12 | 'icon-gf', 13 | { 'icon-gf-critical': status === 'critical' || status === 'problem' || status === 'warning' }, 14 | { 'icon-gf-online': status === 'online' || status === 'ok' } 15 | ); 16 | 17 | return ; 18 | }; 19 | 20 | export default GFHeartIcon; 21 | -------------------------------------------------------------------------------- /src/components/MetricPicker/constants.ts: -------------------------------------------------------------------------------- 1 | export const MENU_MAX_HEIGHT = 300; // max height for the picker's dropdown menu 2 | export const METRIC_PICKER_WIDTH = 360; 3 | -------------------------------------------------------------------------------- /src/components/Modal/ModalController.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { provideTheme } from '../ConfigProvider/ConfigProvider'; 4 | 5 | interface ModalWrapperProps { 6 | showModal: (component: React.ComponentType, props: T) => void; 7 | hideModal: () => void; 8 | } 9 | 10 | type ModalWrapper = FC; 11 | 12 | interface Props { 13 | children: ModalWrapper; 14 | } 15 | 16 | interface State { 17 | component: React.ComponentType | null; 18 | props: any; 19 | } 20 | 21 | export class ModalController extends React.Component { 22 | modalRoot = document.body; 23 | modalNode = document.createElement('div'); 24 | 25 | constructor(props: Props) { 26 | super(props); 27 | this.state = { 28 | component: null, 29 | props: {}, 30 | }; 31 | } 32 | 33 | showModal = (component: React.ComponentType, props: any) => { 34 | this.setState({ 35 | component, 36 | props, 37 | }); 38 | }; 39 | 40 | hideModal = () => { 41 | this.modalRoot.removeChild(this.modalNode); 42 | this.setState({ 43 | component: null, 44 | props: {}, 45 | }); 46 | }; 47 | 48 | renderModal() { 49 | const { component, props } = this.state; 50 | if (!component) { 51 | return null; 52 | } 53 | 54 | this.modalRoot.appendChild(this.modalNode); 55 | const modal = React.createElement(provideTheme(component), props); 56 | return ReactDOM.createPortal(modal, this.modalNode); 57 | } 58 | 59 | render() { 60 | const { children } = this.props; 61 | const ChildrenComponent = children; 62 | 63 | return ( 64 | <> 65 | 66 | {this.renderModal()} 67 | 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/components/index.ts: -------------------------------------------------------------------------------- 1 | export { GFHeartIcon } from './GFHeartIcon/GFHeartIcon'; 2 | export { FAIcon } from './FAIcon/FAIcon'; 3 | export { AckButton } from './AckButton/AckButton'; 4 | export { ExploreButton } from './ExploreButton/ExploreButton'; 5 | export { ExecScriptButton } from './ExecScriptButton/ExecScriptButton'; 6 | export { ModalController } from './Modal/ModalController'; 7 | export { MetricPicker } from './MetricPicker/MetricPicker'; 8 | -------------------------------------------------------------------------------- /src/datasource/components/Divider.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import React from 'react'; 3 | 4 | import { GrafanaTheme2 } from '@grafana/data'; 5 | import { useStyles2 } from '@grafana/ui'; 6 | 7 | // this custom component is necessary because the Grafana UI component is not backwards compatible with Grafana < 10.1.0 8 | export const Divider = () => { 9 | const styles = useStyles2(getStyles); 10 | return
; 11 | }; 12 | 13 | const getStyles = (theme: GrafanaTheme2) => { 14 | return { 15 | horizontalDivider: css({ 16 | borderTop: `1px solid ${theme.colors.border.weak}`, 17 | margin: theme.spacing(2, 0), 18 | width: '100%', 19 | }), 20 | }; 21 | }; 22 | -------------------------------------------------------------------------------- /src/datasource/components/FunctionEditor/AddZabbixFunction.tsx: -------------------------------------------------------------------------------- 1 | import { css, cx } from '@emotion/css'; 2 | import React, { useMemo, useState } from 'react'; 3 | import { GrafanaTheme2 } from '@grafana/data'; 4 | import { Button, ClickOutsideWrapper, Icon, Input, Menu, useStyles2, useTheme2 } from '@grafana/ui'; 5 | import { FuncDef } from '../../types/query'; 6 | import { getCategories } from '../../metricFunctions'; 7 | 8 | // import { mapFuncDefsToSelectables } from './helpers'; 9 | 10 | type Props = { 11 | // funcDefs: MetricFunc; 12 | onFuncAdd: (def: FuncDef) => void; 13 | }; 14 | 15 | export function AddZabbixFunction({ onFuncAdd }: Props) { 16 | const [showMenu, setShowMenu] = useState(false); 17 | const styles = useStyles2(getStyles); 18 | const theme = useTheme2(); 19 | 20 | const onFuncAddInternal = (def: FuncDef) => { 21 | onFuncAdd(def); 22 | setShowMenu(false); 23 | }; 24 | 25 | const onSearch = (e: React.FormEvent) => { 26 | console.log(e.currentTarget.value); 27 | }; 28 | 29 | const onClickOutside = () => { 30 | setShowMenu(false); 31 | }; 32 | 33 | const menuItems = useMemo(() => buildMenuItems(onFuncAddInternal), [onFuncAdd]); 34 | 35 | return ( 36 |
37 | {!showMenu && ( 38 |
53 | ); 54 | } 55 | 56 | function buildMenuItems(onClick: (func: FuncDef) => void) { 57 | const categories = getCategories(); 58 | const menuItems: JSX.Element[] = []; 59 | for (const categoryName in categories) { 60 | const functions = categories[categoryName]; 61 | const subItems = functions.map((f) => onClick(f)} />); 62 | menuItems.push(); 63 | } 64 | return menuItems; 65 | } 66 | 67 | function getStyles(theme: GrafanaTheme2) { 68 | return { 69 | button: css` 70 | margin-right: ${theme.spacing(0.5)}; 71 | `, 72 | }; 73 | } 74 | -------------------------------------------------------------------------------- /src/datasource/components/FunctionEditor/FunctionEditor.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import React from 'react'; 3 | import { FunctionEditorControlsProps, FunctionEditorControls } from './FunctionEditorControls'; 4 | 5 | import { useStyles2, Tooltip } from '@grafana/ui'; 6 | import { GrafanaTheme2 } from '@grafana/data'; 7 | import { MetricFunc } from '../../types/query'; 8 | 9 | interface FunctionEditorProps extends FunctionEditorControlsProps { 10 | func: MetricFunc; 11 | } 12 | 13 | const getStyles = (theme: GrafanaTheme2) => { 14 | return { 15 | icon: css` 16 | margin-right: ${theme.spacing(0.5)}; 17 | `, 18 | label: css({ 19 | fontWeight: theme.typography.fontWeightMedium, 20 | fontSize: theme.typography.bodySmall.fontSize, // to match .gf-form-label 21 | cursor: 'pointer', 22 | display: 'inline-block', 23 | }), 24 | }; 25 | }; 26 | 27 | export const FunctionEditor: React.FC = ({ onMoveLeft, onMoveRight, func, ...props }) => { 28 | const styles = useStyles2(getStyles); 29 | 30 | const renderContent = ({ updatePopperPosition }: any) => ( 31 | { 35 | onMoveLeft(func); 36 | updatePopperPosition(); 37 | }} 38 | onMoveRight={() => { 39 | onMoveRight(func); 40 | updatePopperPosition(); 41 | }} 42 | /> 43 | ); 44 | 45 | return ( 46 | 47 | {func.def.name} 48 | 49 | ); 50 | }; 51 | -------------------------------------------------------------------------------- /src/datasource/components/FunctionEditor/FunctionEditorControls.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Icon } from '@grafana/ui'; 3 | import { MetricFunc } from '../../types/query'; 4 | 5 | const DOCS_FUNC_REF_URL = 'https://grafana.com/docs/plugins/alexanderzobnin-zabbix-app/latest/reference/functions/'; 6 | 7 | export interface FunctionEditorControlsProps { 8 | onMoveLeft: (func: MetricFunc) => void; 9 | onMoveRight: (func: MetricFunc) => void; 10 | onRemove: (func: MetricFunc) => void; 11 | } 12 | 13 | const FunctionHelpButton = (props: { description?: string; name: string }) => { 14 | return ( 15 | { 19 | window.open(`${DOCS_FUNC_REF_URL}#${props.name}`, '_blank'); 20 | }} 21 | /> 22 | ); 23 | }; 24 | 25 | export const FunctionEditorControls = ( 26 | props: FunctionEditorControlsProps & { 27 | func: MetricFunc; 28 | } 29 | ) => { 30 | const { func, onMoveLeft, onMoveRight, onRemove } = props; 31 | return ( 32 |
39 | onMoveLeft(func)} /> 40 | 41 | onRemove(func)} /> 42 | onMoveRight(func)} /> 43 |
44 | ); 45 | }; 46 | -------------------------------------------------------------------------------- /src/datasource/components/FunctionEditor/FunctionParamEditor.tsx: -------------------------------------------------------------------------------- 1 | import { css } from '@emotion/css'; 2 | import React from 'react'; 3 | 4 | import { GrafanaTheme2, SelectableValue } from '@grafana/data'; 5 | import { Segment, SegmentInput, useStyles2 } from '@grafana/ui'; 6 | 7 | export type EditableParam = { 8 | name: string; 9 | value: string; 10 | optional: boolean; 11 | multiple: boolean; 12 | options: Array>; 13 | }; 14 | 15 | type FieldEditorProps = { 16 | editableParam: EditableParam; 17 | onChange: (value: string) => void; 18 | onExpandedChange: (expanded: boolean) => void; 19 | autofocus: boolean; 20 | }; 21 | 22 | /** 23 | * Render a function parameter with a segment dropdown for multiple options or simple input. 24 | */ 25 | export function FunctionParamEditor({ editableParam, onChange, onExpandedChange, autofocus }: FieldEditorProps) { 26 | const styles = useStyles2(getStyles); 27 | 28 | if (editableParam.options?.length > 0) { 29 | return ( 30 | { 38 | onChange(value.value || ''); 39 | }} 40 | onExpandedChange={onExpandedChange} 41 | inputMinWidth={150} 42 | allowCustomValue={true} 43 | allowEmptyValue={true} 44 | > 45 | ); 46 | } else { 47 | return ( 48 | { 55 | onChange(value.toString()); 56 | }} 57 | onExpandedChange={onExpandedChange} 58 | // input style 59 | style={{ height: '25px', paddingTop: '2px', marginTop: '2px', paddingLeft: '4px', minWidth: '100px' }} 60 | > 61 | ); 62 | } 63 | } 64 | 65 | const getStyles = (theme: GrafanaTheme2) => ({ 66 | segment: css({ 67 | margin: 0, 68 | padding: 0, 69 | }), 70 | input: css` 71 | margin: 0; 72 | padding: 0; 73 | input { 74 | height: 25px; 75 | }, 76 | `, 77 | }); 78 | -------------------------------------------------------------------------------- /src/datasource/components/FunctionEditor/helpers.ts: -------------------------------------------------------------------------------- 1 | import { SelectableValue } from '@grafana/data'; 2 | import { MetricFunc } from '../../types/query'; 3 | 4 | export type ParamDef = { 5 | name: string; 6 | type: string; 7 | options?: Array; 8 | multiple?: boolean; 9 | optional?: boolean; 10 | version?: string; 11 | }; 12 | 13 | export type EditableParam = { 14 | name: string; 15 | value: string; 16 | optional: boolean; 17 | multiple: boolean; 18 | options: Array>; 19 | }; 20 | 21 | function createEditableParam(paramDef: ParamDef, additional: boolean, value?: string | number): EditableParam { 22 | return { 23 | name: paramDef.name, 24 | value: value?.toString() || '', 25 | optional: !!paramDef.optional || additional, // only first param is required when multiple are allowed 26 | multiple: !!paramDef.multiple, 27 | options: 28 | paramDef.options?.map((option: string | number) => ({ 29 | value: option.toString(), 30 | label: option.toString(), 31 | })) ?? [], 32 | }; 33 | } 34 | 35 | /** 36 | * Create a list of params that can be edited in the function editor. 37 | */ 38 | export function mapFuncInstanceToParams(func: MetricFunc): EditableParam[] { 39 | // list of required parameters (from func.def) 40 | const params: EditableParam[] = func.def.params.map((paramDef: ParamDef, index: number) => 41 | createEditableParam(paramDef, false, func.params[index]) 42 | ); 43 | 44 | // list of additional (multiple or optional) params entered by the user 45 | while (params.length < func.params.length) { 46 | const paramDef = func.def.params[func.def.params.length - 1]; 47 | const value = func.params[params.length]; 48 | params.push(createEditableParam(paramDef, true, value)); 49 | } 50 | 51 | // extra "fake" param to allow adding more multiple values at the end 52 | if (params.length && params[params.length - 1].value && params[params.length - 1]?.multiple) { 53 | const paramDef = func.def.params[func.def.params.length - 1]; 54 | params.push(createEditableParam(paramDef, true, '')); 55 | } 56 | 57 | return params; 58 | } 59 | -------------------------------------------------------------------------------- /src/datasource/components/QueryEditor/ItemIdQueryEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent } from 'react'; 2 | import { InlineField, Input } from '@grafana/ui'; 3 | import { ZabbixMetricsQuery } from '../../types/query'; 4 | import { QueryEditorRow } from './QueryEditorRow'; 5 | 6 | export interface Props { 7 | query: ZabbixMetricsQuery; 8 | onChange: (query: ZabbixMetricsQuery) => void; 9 | } 10 | 11 | export const ItemIdQueryEditor = ({ query, onChange }: Props) => { 12 | const onItemIdsChange = (v: FormEvent) => { 13 | const newValue = v?.currentTarget?.value; 14 | if (newValue !== null) { 15 | onChange({ ...query, itemids: newValue }); 16 | } 17 | }; 18 | 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /src/datasource/components/QueryEditor/QueryEditorRow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { InlineFieldRow, InlineFormLabel } from '@grafana/ui'; 3 | import { css } from '@emotion/css'; 4 | 5 | export const QueryEditorRow = ({ children }: React.PropsWithChildren<{}>) => { 6 | const styles = getStyles(); 7 | 8 | return ( 9 | 10 | {children} 11 | 12 | <> 13 | 14 | 15 | ); 16 | }; 17 | 18 | const getStyles = () => { 19 | return { 20 | rowTerminator: css({ 21 | flexGrow: 1, 22 | }), 23 | }; 24 | }; 25 | -------------------------------------------------------------------------------- /src/datasource/components/QueryEditor/QueryFunctionsEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { swap } from '../../utils'; 3 | import { createFuncInstance } from '../../metricFunctions'; 4 | import { FuncDef, MetricFunc, ZabbixMetricsQuery } from '../../types/query'; 5 | import { QueryEditorRow } from './QueryEditorRow'; 6 | import { InlineFormLabel } from '@grafana/ui'; 7 | import { ZabbixFunctionEditor } from '../FunctionEditor/ZabbixFunctionEditor'; 8 | import { AddZabbixFunction } from '../FunctionEditor/AddZabbixFunction'; 9 | 10 | export interface Props { 11 | query: ZabbixMetricsQuery; 12 | onChange: (query: ZabbixMetricsQuery) => void; 13 | } 14 | 15 | export const QueryFunctionsEditor = ({ query, onChange }: Props) => { 16 | const onFuncParamChange = (func: MetricFunc, index: number, value: string) => { 17 | func.params[index] = value; 18 | const funcIndex = query.functions.findIndex((f) => f === func); 19 | const functions = query.functions; 20 | functions[funcIndex] = func; 21 | onChange({ ...query, functions }); 22 | }; 23 | 24 | const onMoveFuncLeft = (func: MetricFunc) => { 25 | const index = query.functions.indexOf(func); 26 | const functions = swap(query.functions, index, index - 1); 27 | onChange({ ...query, functions }); 28 | }; 29 | 30 | const onMoveFuncRight = (func: MetricFunc) => { 31 | const index = query.functions.indexOf(func); 32 | const functions = swap(query.functions, index, index + 1); 33 | onChange({ ...query, functions }); 34 | }; 35 | 36 | const onRemoveFunc = (func: MetricFunc) => { 37 | const functions = query.functions?.filter((f) => f !== func); 38 | onChange({ ...query, functions }); 39 | }; 40 | 41 | const onFuncAdd = (def: FuncDef) => { 42 | const newFunc = createFuncInstance(def); 43 | newFunc.added = true; 44 | let functions = query.functions.concat(newFunc); 45 | functions = moveAliasFuncLast(functions); 46 | 47 | // if ((newFunc.params.length && newFunc.added) || newFunc.def.params.length === 0) { 48 | // } 49 | onChange({ ...query, functions }); 50 | }; 51 | 52 | return ( 53 | 54 | Functions 55 | {query.functions?.map((f, i) => { 56 | return ( 57 | 65 | ); 66 | })} 67 | 68 | 69 | ); 70 | }; 71 | 72 | function moveAliasFuncLast(functions: MetricFunc[]) { 73 | const aliasFuncIndex = functions.findIndex((func) => func.def.category === 'Alias'); 74 | 75 | console.log(aliasFuncIndex); 76 | if (aliasFuncIndex >= 0) { 77 | const aliasFunc = functions[aliasFuncIndex]; 78 | functions.splice(aliasFuncIndex, 1); 79 | functions.push(aliasFunc); 80 | } 81 | return functions; 82 | } 83 | -------------------------------------------------------------------------------- /src/datasource/components/QueryEditor/utils.ts: -------------------------------------------------------------------------------- 1 | import { getTemplateSrv } from '@grafana/runtime'; 2 | 3 | export const getVariableOptions = () => { 4 | const variables = getTemplateSrv() 5 | .getVariables() 6 | .filter((v) => { 7 | return v.type !== 'datasource' && v.type !== 'interval'; 8 | }); 9 | return variables?.map((v) => ({ 10 | value: `$${v.name}`, 11 | label: `$${v.name}`, 12 | })); 13 | }; 14 | -------------------------------------------------------------------------------- /src/datasource/components/ZabbixInput.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { css } from '@emotion/css'; 3 | import { EventsWithValidation, ValidationEvents, useStyles2 } from '@grafana/ui'; 4 | import { GrafanaTheme2 } from '@grafana/data'; 5 | import { isRegex, variableRegex } from '../utils'; 6 | 7 | import * as grafanaUi from '@grafana/ui'; 8 | const Input = (grafanaUi as any).Input || (grafanaUi as any).LegacyForms?.Input; 9 | 10 | const variablePattern = RegExp(`^${variableRegex.source}`); 11 | 12 | const getStyles = (theme: GrafanaTheme2) => ({ 13 | inputRegex: css` 14 | color: ${theme.colors.warning.main}; 15 | `, 16 | inputVariable: css` 17 | color: ${theme.colors.action.focus}; 18 | `, 19 | }); 20 | 21 | const zabbixInputValidationEvents: ValidationEvents = { 22 | [EventsWithValidation.onBlur]: [ 23 | { 24 | rule: (value) => { 25 | if (!value) { 26 | return true; 27 | } 28 | if (value.length > 1 && value[0] === '/') { 29 | if (value[value.length - 1] !== '/') { 30 | return false; 31 | } 32 | } 33 | return true; 34 | }, 35 | errorMessage: 'Not a valid regex', 36 | }, 37 | { 38 | rule: (value) => { 39 | if (value === '*') { 40 | return false; 41 | } 42 | return true; 43 | }, 44 | errorMessage: 'Wildcards not supported. Use /.*/ instead', 45 | }, 46 | ], 47 | }; 48 | 49 | export const ZabbixInput: FC = ({ value, ref, validationEvents, ...restProps }) => { 50 | const styles = useStyles2(getStyles); 51 | 52 | let inputClass = styles.inputRegex; 53 | if (variablePattern.test(value as string)) { 54 | inputClass = styles.inputVariable; 55 | } else if (isRegex(value)) { 56 | inputClass = styles.inputRegex; 57 | } 58 | 59 | return ; 60 | }; 61 | -------------------------------------------------------------------------------- /src/datasource/constants.ts: -------------------------------------------------------------------------------- 1 | // Plugin IDs 2 | export const ZABBIX_PROBLEMS_PANEL_ID = 'alexanderzobnin-zabbix-triggers-panel'; 3 | export const ZABBIX_DS_ID = 'alexanderzobnin-zabbix-datasource'; 4 | 5 | // Data point 6 | export const DATAPOINT_VALUE = 0; 7 | export const DATAPOINT_TS = 1; 8 | 9 | // Editor modes 10 | export const MODE_METRICS = '0'; 11 | export const MODE_ITSERVICE = '1'; 12 | export const MODE_TEXT = '2'; 13 | export const MODE_ITEMID = '3'; 14 | export const MODE_TRIGGERS = '4'; 15 | export const MODE_PROBLEMS = '5'; 16 | export const MODE_MACROS = '6'; 17 | 18 | // Triggers severity 19 | export const SEV_NOT_CLASSIFIED = 0; 20 | export const SEV_INFORMATION = 1; 21 | export const SEV_WARNING = 2; 22 | export const SEV_AVERAGE = 3; 23 | export const SEV_HIGH = 4; 24 | export const SEV_DISASTER = 5; 25 | 26 | export const SHOW_ALL_TRIGGERS = [0, 1]; 27 | export const SHOW_ALL_EVENTS = [0, 1]; 28 | export const SHOW_OK_EVENTS = 1; 29 | 30 | // Acknowledge 31 | export const ZBX_ACK_ACTION_NONE = 0; 32 | export const ZBX_ACK_ACTION_CLOSE = 1; 33 | export const ZBX_ACK_ACTION_ACK = 2; 34 | export const ZBX_ACK_ACTION_ADD_MESSAGE = 4; 35 | export const ZBX_ACK_ACTION_CHANGE_SEVERITY = 8; 36 | 37 | export const TRIGGER_SEVERITY = [ 38 | { val: 0, text: 'Not classified' }, 39 | { val: 1, text: 'Information' }, 40 | { val: 2, text: 'Warning' }, 41 | { val: 3, text: 'Average' }, 42 | { val: 4, text: 'High' }, 43 | { val: 5, text: 'Disaster' }, 44 | ]; 45 | 46 | /** Minimum interval for SLA over time (1 hour) */ 47 | export const MIN_SLA_INTERVAL = 3600; 48 | 49 | export const RANGE_VARIABLE_VALUE = 'range_series'; 50 | 51 | export const DEFAULT_ZABBIX_PROBLEMS_LIMIT = 1001; 52 | -------------------------------------------------------------------------------- /src/datasource/module.ts: -------------------------------------------------------------------------------- 1 | import { DataSourcePlugin } from '@grafana/data'; 2 | import { ZabbixDatasource } from './datasource'; 3 | import { QueryEditor } from './components/QueryEditor'; 4 | import { ZabbixVariableQueryEditor } from './components/VariableQueryEditor'; 5 | import { ConfigEditor } from './components/ConfigEditor'; 6 | 7 | export const plugin = new DataSourcePlugin(ZabbixDatasource) 8 | .setConfigEditor(ConfigEditor) 9 | .setQueryEditor(QueryEditor) 10 | .setVariableQueryEditor(ZabbixVariableQueryEditor); 11 | -------------------------------------------------------------------------------- /src/datasource/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "datasource", 3 | "name": "Zabbix", 4 | "id": "alexanderzobnin-zabbix-datasource", 5 | "metrics": true, 6 | "annotations": true, 7 | "backend": true, 8 | "alerting": true, 9 | "executable": "gpx_zabbix-datasource", 10 | "includes": [ 11 | { 12 | "type": "dashboard", 13 | "name": "Zabbix System Status", 14 | "path": "dashboards/zabbix_system_status.json" 15 | }, 16 | { 17 | "type": "dashboard", 18 | "name": "Zabbix Template Linux Server", 19 | "path": "dashboards/template_linux_server.json" 20 | }, 21 | { 22 | "type": "dashboard", 23 | "name": "Zabbix Server Dashboard", 24 | "path": "dashboards/zabbix_server_dashboard.json" 25 | } 26 | ], 27 | "queryOptions": { 28 | "maxDataPoints": true 29 | }, 30 | "info": { 31 | "author": { 32 | "name": "Grafana Labs", 33 | "url": "https://grafana.com" 34 | }, 35 | "logos": { 36 | "small": "img/icn-zabbix-datasource.svg", 37 | "large": "img/icn-zabbix-datasource.svg" 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/datasource/query_help.md: -------------------------------------------------------------------------------- 1 | #### Max data points 2 | 3 | Override max data points, automatically set to graph width in pixels. Grafana-Zabbix plugin uses maxDataPoints parameter to consolidate the real number of values down to this number. If there are more real values, then by default they will be consolidated using averages. This could hide real peaks and max values in your series. Point consolidation will affect series legend values (min,max,total,current). 4 | 5 | #### Query Mode 6 | 7 | ##### Merics 8 | 9 | Data from numeric items. 10 | 11 | ##### Text 12 | 13 | Data from items with `Character`, `Text` or `Log` type. 14 | 15 | ##### IT Services 16 | 17 | Time series representation of IT Services data 18 | 19 | ###### IT service property 20 | 21 | Zabbix returns the following availability information about IT service: 22 | 23 | - Status - current status of the IT service 24 | - SLA - SLA for the given time interval 25 | - OK time - time the service was in OK state, in seconds 26 | - Problem time - time the service was in problem state, in seconds 27 | - Down time - time the service was in scheduled downtime, in seconds 28 | 29 | ##### Item ID 30 | 31 | Data from items with specified ID's (comma separated). 32 | This mode is suitable for rendering charts in grafana by passing itemids as url params. 33 | 34 | 1. Create multivalue template variable with type _Custom_, for example, `itemids`. 35 | 2. Create graph with desired parameters and use `$itemids` in _Item IDs_ filed. 36 | 3. Save dashboard. 37 | 4. Click to graph title and select _Share_ -> _Direct link rendered image_. 38 | 5. Use this URL for graph png image and set `var-itemids` param to desired IDs. Note, for multiple IDs you should pass multiple params, like `&var-itemids=28276&var-itemids=28277`. 39 | 40 | ##### Triggers 41 | 42 | Active triggers count for selected hosts or table data like Zabbix _System status_ panel on the main dashboard. 43 | 44 | #### Documentation links 45 | 46 | - [Grafana-Zabbix Documentation](https://grafana.com/docs/plugins/alexanderzobnin-zabbix-app/latest/) 47 | -------------------------------------------------------------------------------- /src/datasource/responseHandler.spec.ts: -------------------------------------------------------------------------------- 1 | import { FieldType, MutableDataFrame, TIME_SERIES_TIME_FIELD_NAME } from '@grafana/data'; 2 | import { convertToWide } from './responseHandler'; 3 | 4 | describe('convertToWide', () => { 5 | it('merge multiple SLI frames correctly', () => { 6 | let frames = convertToWide([ 7 | new MutableDataFrame({ 8 | name: 'SLI', 9 | fields: [ 10 | { name: TIME_SERIES_TIME_FIELD_NAME, values: [1], type: FieldType.time }, 11 | { name: TIME_SERIES_TIME_FIELD_NAME, values: [1.1], type: FieldType.number }, 12 | ], 13 | }), 14 | new MutableDataFrame({ 15 | name: 'SLI', 16 | fields: [ 17 | { name: TIME_SERIES_TIME_FIELD_NAME, values: [1], type: FieldType.time }, 18 | { name: TIME_SERIES_TIME_FIELD_NAME, values: [1.2], type: FieldType.number }, 19 | ], 20 | }), 21 | ]); 22 | expect(frames.length).toStrictEqual(1); 23 | expect(frames[0].fields.length).toStrictEqual(3); 24 | expect(frames[0].fields[0].values.at(0)).toStrictEqual(1); 25 | expect(frames[0].fields[1].values.at(0)).toStrictEqual(1.1); 26 | expect(frames[0].fields[2].values.at(0)).toStrictEqual(1.2); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/datasource/specs/dbConnector.test.ts: -------------------------------------------------------------------------------- 1 | import { DBConnector } from '../zabbix/connectors/dbConnector'; 2 | 3 | const loadDatasourceMock = jest.fn().mockResolvedValue({ id: 42, name: 'foo', meta: {} }); 4 | const getAllMock = jest.fn().mockReturnValue([{ id: 42, name: 'foo', meta: {} }]); 5 | 6 | jest.mock('@grafana/runtime', () => ({ 7 | getDataSourceSrv: () => ({ 8 | get: loadDatasourceMock, 9 | getList: getAllMock, 10 | }), 11 | })); 12 | 13 | describe('DBConnector', () => { 14 | const ctx: any = {}; 15 | 16 | describe('When init DB connector', () => { 17 | beforeEach(() => { 18 | ctx.options = { 19 | datasourceId: 42, 20 | datasourceName: undefined, 21 | }; 22 | 23 | loadDatasourceMock.mockClear(); 24 | getAllMock.mockClear(); 25 | }); 26 | 27 | it('should try to load datasource by name first', () => { 28 | const dbConnector = new DBConnector({ datasourceName: 'bar' }); 29 | dbConnector.loadDBDataSource(); 30 | expect(getAllMock).not.toHaveBeenCalled(); 31 | expect(loadDatasourceMock).toHaveBeenCalledWith('bar'); 32 | }); 33 | 34 | it('should load datasource by id if name not present', () => { 35 | const dbConnector = new DBConnector({ datasourceId: 42 }); 36 | dbConnector.loadDBDataSource(); 37 | expect(getAllMock).toHaveBeenCalled(); 38 | expect(loadDatasourceMock).toHaveBeenCalledWith('foo'); 39 | }); 40 | 41 | it('should throw error if no name and id specified', () => { 42 | ctx.options = {}; 43 | const dbConnector = new DBConnector(ctx.options); 44 | return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source name should be specified'); 45 | }); 46 | 47 | it('should throw error if datasource with given id is not found', () => { 48 | ctx.options.datasourceId = 45; 49 | const dbConnector = new DBConnector(ctx.options); 50 | return expect(dbConnector.loadDBDataSource()).rejects.toBe('Data Source with ID 45 not found'); 51 | }); 52 | }); 53 | }); 54 | -------------------------------------------------------------------------------- /src/datasource/specs/migrations.test.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { migrateDSConfig, DS_CONFIG_SCHEMA } from '../migrations'; 3 | 4 | describe('Migrations', () => { 5 | let ctx: any = {}; 6 | 7 | describe('When migrating datasource config', () => { 8 | beforeEach(() => { 9 | ctx.jsonData = { 10 | dbConnection: { 11 | enable: true, 12 | datasourceId: 1, 13 | }, 14 | }; 15 | }); 16 | 17 | it('should change direct DB connection setting to flat style', () => { 18 | migrateDSConfig(ctx.jsonData); 19 | expect(ctx.jsonData).toMatchObject({ 20 | dbConnectionEnable: true, 21 | dbConnectionDatasourceId: 1, 22 | schema: DS_CONFIG_SCHEMA, 23 | }); 24 | }); 25 | 26 | it('should not touch anything if schema is up to date', () => { 27 | ctx.jsonData = { 28 | futureOptionOne: 'foo', 29 | futureOptionTwo: 'bar', 30 | schema: DS_CONFIG_SCHEMA, 31 | }; 32 | migrateDSConfig(ctx.jsonData); 33 | expect(ctx.jsonData).toMatchObject({ 34 | futureOptionOne: 'foo', 35 | futureOptionTwo: 'bar', 36 | schema: DS_CONFIG_SCHEMA, 37 | }); 38 | expect(ctx.jsonData.dbConnectionEnable).toBeUndefined(); 39 | expect(ctx.jsonData.dbConnectionDatasourceId).toBeUndefined(); 40 | }); 41 | }); 42 | 43 | describe('When handling provisioned datasource config', () => { 44 | beforeEach(() => { 45 | ctx.jsonData = { 46 | username: 'zabbix', 47 | password: 'zabbix', 48 | trends: true, 49 | trendsFrom: '7d', 50 | trendsRange: '4d', 51 | cacheTTL: '1h', 52 | alerting: true, 53 | addThresholds: false, 54 | alertingMinSeverity: 3, 55 | disableReadOnlyUsersAck: true, 56 | dbConnectionEnable: true, 57 | dbConnectionDatasourceName: 'MySQL Zabbix', 58 | dbConnectionRetentionPolicy: 'one_year', 59 | }; 60 | }); 61 | 62 | it('should not touch anything if schema is up to date', () => { 63 | const originalConf = _.cloneDeep(ctx.jsonData); 64 | migrateDSConfig(ctx.jsonData); 65 | expect(ctx.jsonData).toMatchObject(originalConf); 66 | expect(ctx.jsonData.dbConnectionEnable).toBe(true); 67 | expect(ctx.jsonData.dbConnectionDatasourceName).toBeDefined(); 68 | }); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/datasource/specs/timeseries.spec.ts: -------------------------------------------------------------------------------- 1 | // import _ from 'lodash'; 2 | import ts from '../timeseries'; 3 | 4 | describe('timeseries processing functions', () => { 5 | describe('sumSeries()', () => { 6 | it('should properly sum series', (done) => { 7 | let series = [ 8 | [ 9 | [0, 1], 10 | [1, 2], 11 | [1, 3], 12 | ], 13 | [ 14 | [2, 1], 15 | [3, 2], 16 | [4, 3], 17 | ], 18 | ]; 19 | 20 | let expected = [ 21 | [2, 1], 22 | [4, 2], 23 | [5, 3], 24 | ]; 25 | 26 | let result = ts.sumSeries(series); 27 | expect(result).toEqual(expected); 28 | done(); 29 | }); 30 | 31 | it('should properly sum series with nulls', (done) => { 32 | // issue #286 33 | let series = [ 34 | [ 35 | [1, 1], 36 | [1, 2], 37 | [1, 3], 38 | ], 39 | [ 40 | [3, 2], 41 | [4, 3], 42 | ], 43 | ]; 44 | 45 | let expected = [ 46 | [1, 1], 47 | [4, 2], 48 | [5, 3], 49 | ]; 50 | 51 | let result = ts.sumSeries(series); 52 | expect(result).toEqual(expected); 53 | done(); 54 | }); 55 | 56 | it('should properly offset metric', (done) => { 57 | let points = [ 58 | [1, 1], 59 | [-4, 2], 60 | [2, 3], 61 | ]; 62 | 63 | let expected = [ 64 | [101, 1], 65 | [96, 2], 66 | [102, 3], 67 | ]; 68 | 69 | let result = ts.offset(points, 100); 70 | expect(result).toEqual(expected); 71 | done(); 72 | }); 73 | }); 74 | }); 75 | -------------------------------------------------------------------------------- /src/datasource/tracking.ts: -------------------------------------------------------------------------------- 1 | import { CoreApp, DataQueryRequest } from '@grafana/data'; 2 | import { reportInteraction } from '@grafana/runtime'; 3 | import { ZabbixMetricsQuery } from './types/query'; 4 | import { 5 | MODE_ITEMID, 6 | MODE_ITSERVICE, 7 | MODE_MACROS, 8 | MODE_METRICS, 9 | MODE_PROBLEMS, 10 | MODE_TEXT, 11 | MODE_TRIGGERS, 12 | } from './constants'; 13 | 14 | export const trackRequest = (request: DataQueryRequest): void => { 15 | if (request.app === CoreApp.Dashboard || request.app === CoreApp.PanelViewer) { 16 | return; 17 | } 18 | 19 | request.targets.forEach((target) => { 20 | const properties: any = { 21 | app: request.app, 22 | }; 23 | 24 | switch (target.queryType) { 25 | case MODE_METRICS: 26 | properties.queryType = 'Metrics'; 27 | break; 28 | case MODE_ITSERVICE: 29 | properties.queryType = 'Services'; 30 | break; 31 | case MODE_TEXT: 32 | properties.queryType = 'Text'; 33 | break; 34 | case MODE_ITEMID: 35 | properties.queryType = 'Item Id'; 36 | break; 37 | case MODE_TRIGGERS: 38 | properties.queryType = 'Triggers'; 39 | break; 40 | case MODE_PROBLEMS: 41 | properties.queryType = 'Problems'; 42 | break; 43 | case MODE_MACROS: 44 | properties.queryType = 'Macros'; 45 | break; 46 | } 47 | 48 | reportInteraction('grafana_zabbix_query_executed', properties); 49 | }); 50 | }; 51 | -------------------------------------------------------------------------------- /src/datasource/types/config.ts: -------------------------------------------------------------------------------- 1 | import { DataSourceJsonData } from '@grafana/data'; 2 | 3 | export enum ZabbixAuthType { 4 | UserLogin = 'userLogin', 5 | Token = 'token', 6 | } 7 | 8 | export type ZabbixDSOptions = { 9 | authType?: ZabbixAuthType; 10 | username: string; 11 | password?: string; 12 | trends: boolean; 13 | trendsFrom: string; 14 | trendsRange: string; 15 | cacheTTL: string; 16 | timeout?: number; 17 | dbConnectionEnable: boolean; 18 | dbConnectionDatasourceId?: number; 19 | dbConnectionDatasourceName?: string; 20 | dbConnectionRetentionPolicy?: string; 21 | disableReadOnlyUsersAck: boolean; 22 | disableDataAlignment: boolean; 23 | enableSecureSocksProxy?: boolean; 24 | } & DataSourceJsonData; 25 | 26 | type ZabbixSecureJSONDataKeys = 'password' | 'apiToken'; 27 | 28 | export type ZabbixSecureJSONData = Partial>; 29 | -------------------------------------------------------------------------------- /src/datasource/types/query.ts: -------------------------------------------------------------------------------- 1 | import { DataQuery } from '@grafana/schema'; 2 | import * as c from './../constants'; 3 | 4 | export type QueryType = 5 | | typeof c.MODE_METRICS 6 | | typeof c.MODE_ITSERVICE 7 | | typeof c.MODE_TEXT 8 | | typeof c.MODE_ITEMID 9 | | typeof c.MODE_TRIGGERS 10 | | typeof c.MODE_PROBLEMS 11 | | typeof c.MODE_MACROS; 12 | 13 | type BaseQuery = { queryType: QueryType; datasourceId: number } & DataQuery; 14 | 15 | export type ZabbixMetricsQuery = { 16 | schema: number; 17 | group: { filter: string; name?: string }; 18 | host: { filter: string; name?: string }; 19 | application: { filter: string; name?: string }; 20 | itemTag: { filter: string; name?: string }; 21 | item: { filter: string; name?: string }; 22 | macro: { filter: string; macro?: string }; 23 | textFilter: string; 24 | mode: number; 25 | itemids: string; 26 | useCaptureGroups: boolean; 27 | proxy?: { filter: string }; 28 | trigger?: { filter: string }; 29 | itServiceFilter?: string; 30 | slaFilter?: string; 31 | slaProperty?: any; 32 | slaInterval?: string; 33 | tags?: { filter: string }; 34 | triggers?: { minSeverity: number; acknowledged: number; count: boolean }; 35 | countTriggersBy?: 'problems' | 'items' | ''; 36 | evaltype?: ZabbixTagEvalType; 37 | functions?: MetricFunc[]; 38 | options?: ZabbixQueryOptions; 39 | // Problems 40 | showProblems?: ShowProblemTypes; 41 | // Deprecated 42 | hostFilter?: string; 43 | itemFilter?: string; 44 | macroFilter?: string; 45 | } & BaseQuery; 46 | 47 | export interface ZabbixQueryOptions { 48 | showDisabledItems?: boolean; 49 | skipEmptyValues?: boolean; 50 | disableDataAlignment?: boolean; 51 | useZabbixValueMapping?: boolean; 52 | useTrends?: 'default' | 'true' | 'false'; 53 | // Problems options 54 | minSeverity?: number; 55 | sortProblems?: string; 56 | acknowledged?: number; 57 | hostsInMaintenance?: boolean; 58 | hostProxy?: boolean; 59 | limit?: number; 60 | useTimeRange?: boolean; 61 | severities?: number[]; 62 | count?: boolean; 63 | 64 | // Annotations 65 | showOkEvents?: boolean; 66 | hideAcknowledged?: boolean; 67 | showHostname?: boolean; 68 | } 69 | 70 | export interface MetricFunc { 71 | text: string; 72 | params: Array; 73 | def: FuncDef; 74 | added?: boolean; 75 | } 76 | 77 | export interface FuncDef { 78 | name: string; 79 | params: ParamDef[]; 80 | defaultParams: Array; 81 | category?: string; 82 | shortName?: any; 83 | fake?: boolean; 84 | version?: string; 85 | description?: string; 86 | /** 87 | * True if the function was not found on the list of available function descriptions. 88 | */ 89 | unknown?: boolean; 90 | } 91 | 92 | export type ParamDef = { 93 | name: string; 94 | type: string; 95 | options?: Array; 96 | multiple?: boolean; 97 | optional?: boolean; 98 | version?: string; 99 | }; 100 | 101 | export enum ShowProblemTypes { 102 | Problems = 'problems', 103 | Recent = 'recent', 104 | History = 'history', 105 | } 106 | 107 | export enum ZabbixTagEvalType { 108 | AndOr = '0', 109 | Or = '2', 110 | } 111 | -------------------------------------------------------------------------------- /src/datasource/zabbix/connectors/dbConnector.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { getDataSourceSrv } from '@grafana/runtime'; 3 | 4 | export const DEFAULT_QUERY_LIMIT = 10000; 5 | 6 | export const HISTORY_TO_TABLE_MAP = { 7 | '0': 'history', 8 | '1': 'history_str', 9 | '2': 'history_log', 10 | '3': 'history_uint', 11 | '4': 'history_text', 12 | }; 13 | 14 | export const TREND_TO_TABLE_MAP = { 15 | '0': 'trends', 16 | '3': 'trends_uint', 17 | }; 18 | 19 | export const consolidateByFunc = { 20 | avg: 'AVG', 21 | min: 'MIN', 22 | max: 'MAX', 23 | sum: 'SUM', 24 | count: 'COUNT', 25 | }; 26 | 27 | export const consolidateByTrendColumns = { 28 | avg: 'value_avg', 29 | min: 'value_min', 30 | max: 'value_max', 31 | sum: 'num*value_avg', // sum of sums inside the one-hour trend period 32 | }; 33 | 34 | /** 35 | * Base class for external history database connectors. Subclasses should implement `getHistory()`, `getTrends()` and 36 | * `testDataSource()` methods, which describe how to fetch data from source other than Zabbix API. 37 | */ 38 | export class DBConnector { 39 | protected datasourceId: any; 40 | private datasourceName: any; 41 | protected datasourceTypeId: any; 42 | // private datasourceTypeName: any; 43 | 44 | constructor(options) { 45 | this.datasourceId = options.datasourceId; 46 | this.datasourceName = options.datasourceName; 47 | this.datasourceTypeId = null; 48 | // this.datasourceTypeName = null; 49 | } 50 | 51 | static loadDatasource(dsId, dsName) { 52 | if (!dsName && dsId !== undefined) { 53 | const ds = _.find(getDataSourceSrv().getList(), { id: dsId }); 54 | if (!ds) { 55 | return Promise.reject(`Data Source with ID ${dsId} not found`); 56 | } 57 | dsName = ds.name; 58 | } 59 | if (dsName) { 60 | return getDataSourceSrv().get(dsName); 61 | } else { 62 | return Promise.reject(`Data Source name should be specified`); 63 | } 64 | } 65 | 66 | loadDBDataSource() { 67 | return DBConnector.loadDatasource(this.datasourceId, this.datasourceName).then((ds) => { 68 | this.datasourceTypeId = ds.meta.id; 69 | // this.datasourceTypeName = ds.meta.name; 70 | if (!this.datasourceName) { 71 | this.datasourceName = ds.name; 72 | } 73 | if (!this.datasourceId) { 74 | this.datasourceId = ds.id; 75 | } 76 | return ds; 77 | }); 78 | } 79 | } 80 | 81 | export default { 82 | DBConnector, 83 | DEFAULT_QUERY_LIMIT, 84 | HISTORY_TO_TABLE_MAP, 85 | TREND_TO_TABLE_MAP, 86 | consolidateByFunc, 87 | consolidateByTrendColumns, 88 | }; 89 | -------------------------------------------------------------------------------- /src/datasource/zabbix/connectors/sql/mysql.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MySQL queries 3 | */ 4 | 5 | function historyQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) { 6 | const time_expression = `clock DIV ${intervalSec} * ${intervalSec}`; 7 | return ` 8 | SELECT CAST(itemid AS CHAR) AS metric, ${time_expression} AS time_sec, ${aggFunction}(value) AS value 9 | FROM ${table} 10 | WHERE itemid IN (${itemids}) 11 | AND clock 12 | > ${timeFrom} 13 | AND clock 14 | < ${timeTill} 15 | GROUP BY ${time_expression}, metric 16 | ORDER BY time_sec ASC 17 | `; 18 | } 19 | 20 | function trendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) { 21 | const time_expression = `clock DIV ${intervalSec} * ${intervalSec}`; 22 | return ` 23 | SELECT CAST(itemid AS CHAR) AS metric, ${time_expression} AS time_sec, ${aggFunction}(${valueColumn}) AS value 24 | FROM ${table} 25 | WHERE itemid IN (${itemids}) 26 | AND clock 27 | > ${timeFrom} 28 | AND clock 29 | < ${timeTill} 30 | GROUP BY ${time_expression}, metric 31 | ORDER BY time_sec ASC 32 | `; 33 | } 34 | 35 | function testQuery() { 36 | return `SELECT CAST(itemid AS CHAR) AS metric, clock AS time_sec, value_avg AS value 37 | FROM trends_uint LIMIT 1`; 38 | } 39 | 40 | const mysql = { 41 | historyQuery, 42 | trendsQuery, 43 | testQuery, 44 | }; 45 | 46 | export default mysql; 47 | -------------------------------------------------------------------------------- /src/datasource/zabbix/connectors/sql/postgres.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Postgres queries 3 | */ 4 | 5 | const ITEMID_FORMAT = 'FM99999999999999999999'; 6 | 7 | function historyQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction) { 8 | const time_expression = `clock / ${intervalSec} * ${intervalSec}`; 9 | return ` 10 | SELECT to_char(itemid, '${ITEMID_FORMAT}') AS metric, ${time_expression} AS time, ${aggFunction}(value) AS value 11 | FROM ${table} 12 | WHERE itemid IN (${itemids}) 13 | AND clock 14 | > ${timeFrom} 15 | AND clock 16 | < ${timeTill} 17 | GROUP BY 1, 2 18 | ORDER BY time ASC 19 | `; 20 | } 21 | 22 | function trendsQuery(itemids, table, timeFrom, timeTill, intervalSec, aggFunction, valueColumn) { 23 | const time_expression = `clock / ${intervalSec} * ${intervalSec}`; 24 | return ` 25 | SELECT to_char(itemid, '${ITEMID_FORMAT}') AS metric, ${time_expression} AS time, ${aggFunction}(${valueColumn}) AS value 26 | FROM ${table} 27 | WHERE itemid IN (${itemids}) 28 | AND clock 29 | > ${timeFrom} 30 | AND clock 31 | < ${timeTill} 32 | GROUP BY 1, 2 33 | ORDER BY time ASC 34 | `; 35 | } 36 | 37 | const TEST_QUERY = ` 38 | SELECT to_char(itemid, '${ITEMID_FORMAT}') AS metric, clock AS time, value_avg AS value 39 | FROM trends_uint LIMIT 1 40 | `; 41 | 42 | function testQuery() { 43 | return TEST_QUERY; 44 | } 45 | 46 | const postgres = { 47 | historyQuery, 48 | trendsQuery, 49 | testQuery, 50 | }; 51 | 52 | export default postgres; 53 | -------------------------------------------------------------------------------- /src/datasource/zabbix/connectors/zabbix_api/types.ts: -------------------------------------------------------------------------------- 1 | export interface JSONRPCRequest { 2 | jsonrpc: '2.0' | string; 3 | method: string; 4 | id: number; 5 | auth?: string | null; 6 | params?: JSONRPCRequestParams; 7 | } 8 | 9 | export interface JSONRPCResponse { 10 | jsonrpc: '2.0' | string; 11 | id: number; 12 | result?: T; 13 | error?: JSONRPCError; 14 | } 15 | 16 | export interface JSONRPCError { 17 | code?: number; 18 | message?: string; 19 | data?: string; 20 | } 21 | 22 | export type JSONRPCRequestParams = { [key: string]: any }; 23 | 24 | export type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'CONNECT' | 'OPTIONS' | 'TRACE'; 25 | 26 | export type GFRequestOptions = { [key: string]: any }; 27 | 28 | export interface ZabbixRequestResponse { 29 | data?: JSONRPCResponse; 30 | } 31 | 32 | export type ZabbixAPIResponse = Promise; 33 | 34 | export type APILoginResponse = string; 35 | 36 | export interface ZBXScript { 37 | scriptid: string; 38 | name?: string; 39 | command?: string; 40 | host_access?: string; 41 | usrgrpid?: string; 42 | groupid?: string; 43 | description?: string; 44 | confirmation?: string; 45 | type?: string; 46 | execute_on?: string; 47 | } 48 | 49 | export interface APIExecuteScriptResponse { 50 | response: 'success' | 'failed'; 51 | value?: string; 52 | } 53 | -------------------------------------------------------------------------------- /src/datasource/zabbix/types.ts: -------------------------------------------------------------------------------- 1 | export type zabbixMethodName = 2 | | 'alert.get' 3 | | 'apiinfo.version' 4 | | 'application.get' 5 | | 'event.acknowledge' 6 | | 'event.get' 7 | | 'history.get' 8 | | 'host.get' 9 | | 'hostgroup.get' 10 | | 'item.get' 11 | | 'problem.get' 12 | | 'proxy.get' 13 | | 'script.execute' 14 | | 'script.get' 15 | | 'service.get' 16 | | 'service.getsla' 17 | | 'sla.get' 18 | | 'sla.getsli' 19 | | 'trend.get' 20 | | 'trigger.get' 21 | | 'user.get' 22 | | 'usermacro.get' 23 | | 'valuemap.get'; 24 | 25 | export interface ZabbixConnector { 26 | getHistory: (items, timeFrom, timeTill) => Promise; 27 | getTrend: (items, timeFrom, timeTill) => Promise; 28 | getItemsByIDs: (itemids) => Promise; 29 | getEvents: (objectids, timeFrom, timeTo, showEvents, limit?) => Promise; 30 | getAlerts: (itemids, timeFrom?, timeTo?) => Promise; 31 | getHostAlerts: (hostids, applicationids, options?) => Promise; 32 | getHostICAlerts: (hostids, applicationids, itemids, options?) => Promise; 33 | getHostPCAlerts: (hostids, applicationids, triggerids, options?) => Promise; 34 | getAcknowledges: (eventids) => Promise; 35 | getITService: (serviceids?) => Promise; 36 | acknowledgeEvent: (eventid, message) => Promise; 37 | getProxies: () => Promise; 38 | getEventAlerts: (eventids) => Promise; 39 | getExtendedEventData: (eventids) => Promise; 40 | getUserMacros: (hostmacroids) => Promise; 41 | getMacros: (hostids: any[]) => Promise; 42 | getVersion: () => Promise; 43 | 44 | getGroups: (groupFilter?) => any; 45 | getHosts: (groupFilter?, hostFilter?) => any; 46 | getApps: (groupFilter?, hostFilter?, appFilter?) => any; 47 | getUMacros: (groupFilter?, hostFilter?, macroFilter?) => any; 48 | getItems: (groupFilter?, hostFilter?, appFilter?, itemTagFilter?, itemFilter?, options?) => any; 49 | getSLA: (itservices, timeRange, target, options?) => any; 50 | 51 | supportsApplications: () => boolean; 52 | } 53 | -------------------------------------------------------------------------------- /src/img/screenshot-annotations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/src/img/screenshot-annotations.png -------------------------------------------------------------------------------- /src/img/screenshot-dashboard01.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/src/img/screenshot-dashboard01.png -------------------------------------------------------------------------------- /src/img/screenshot-metric_editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/src/img/screenshot-metric_editor.png -------------------------------------------------------------------------------- /src/img/screenshot-showcase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/src/img/screenshot-showcase.png -------------------------------------------------------------------------------- /src/img/screenshot-triggers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/grafana-zabbix/ebc24dc543b7cfdce53bf0d645dd7f9d191b8327/src/img/screenshot-triggers.png -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import { AppPlugin } from '@grafana/data'; 2 | import { loadPluginCss } from '@grafana/runtime'; 3 | 4 | loadPluginCss({ 5 | dark: 'plugins/alexanderzobnin-zabbix-app/styles/dark.css', 6 | light: 'plugins/alexanderzobnin-zabbix-app/styles/light.css', 7 | }); 8 | 9 | export const plugin = new AppPlugin<{}>(); 10 | -------------------------------------------------------------------------------- /src/panel-triggers/components/AlertList/AlertAcknowledges.tsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from 'react'; 2 | import { ProblemDTO } from '../../../datasource/types'; 3 | 4 | interface AlertAcknowledgesProps { 5 | problem: ProblemDTO; 6 | onClick: (event?) => void; 7 | } 8 | 9 | export default class AlertAcknowledges extends PureComponent { 10 | handleClick = (event) => { 11 | this.props.onClick(event); 12 | }; 13 | 14 | render() { 15 | const { problem } = this.props; 16 | const ackRows = 17 | problem.acknowledges && 18 | problem.acknowledges.map((ack) => { 19 | return ( 20 | 21 | {ack.time} 22 | {ack.user} 23 | {ack.message} 24 | 25 | ); 26 | }); 27 | return ( 28 |
29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {ackRows} 38 |
TimeUserComments
39 | {problem.showAckButton && ( 40 |
41 | 48 |
49 | )} 50 |
51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/panel-triggers/components/AlertList/AlertIcon.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { cx, css } from '@emotion/css'; 3 | import { GFHeartIcon } from '../../../components'; 4 | import { ProblemDTO } from '../../../datasource/types'; 5 | 6 | interface Props { 7 | problem: ProblemDTO; 8 | color: string; 9 | blink?: boolean; 10 | highlightBackground?: boolean; 11 | } 12 | 13 | export const AlertIcon: FC = ({ problem, color, blink, highlightBackground }) => { 14 | const severity = Number(problem.severity); 15 | const status = problem.value === '1' && severity >= 2 ? 'critical' : 'online'; 16 | 17 | const iconClass = cx('icon-gf', blink && 'zabbix-trigger--blinked'); 18 | 19 | const wrapperClass = cx( 20 | 'alert-rule-item__icon', 21 | !highlightBackground && 22 | css` 23 | color: ${color}; 24 | ` 25 | ); 26 | 27 | return ( 28 |
29 | 30 |
31 | ); 32 | }; 33 | 34 | export default AlertIcon; 35 | -------------------------------------------------------------------------------- /src/panel-triggers/components/ProblemColorEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { FormEvent } from 'react'; 2 | import { StandardEditorProps } from '@grafana/data'; 3 | import { ColorPicker, InlineField, InlineFieldRow, InlineLabel, InlineSwitch, Input, VerticalGroup } from '@grafana/ui'; 4 | import { TriggerSeverity } from '../types'; 5 | 6 | type Props = StandardEditorProps; 7 | 8 | export const ProblemColorEditor = ({ value, onChange }: Props): JSX.Element => { 9 | const onSeverityItemChange = (severity: TriggerSeverity) => { 10 | value.forEach((v, i) => { 11 | if (v.priority === severity.priority) { 12 | value[i] = severity; 13 | } 14 | }); 15 | onChange(value); 16 | }; 17 | 18 | return ( 19 | <> 20 | {value.map((severity, index) => ( 21 | onSeverityItemChange(value)} 25 | /> 26 | ))} 27 | 28 | ); 29 | }; 30 | 31 | interface ProblemColorEditorRowProps { 32 | value: TriggerSeverity; 33 | onChange: (value?: TriggerSeverity) => void; 34 | } 35 | 36 | export const ProblemColorEditorRow = ({ value, onChange }: ProblemColorEditorRowProps): JSX.Element => { 37 | const onSeverityNameChange = (v: FormEvent) => { 38 | const newValue = v?.currentTarget?.value; 39 | if (newValue !== null) { 40 | onChange({ ...value, severity: newValue }); 41 | } 42 | }; 43 | 44 | return ( 45 | 46 | 47 | 48 | 49 | 50 | 51 | onChange({ ...value, color })} /> 52 | 53 | 54 | onChange({ ...value, show: !value.show })} /> 55 | 56 | 57 | 58 | ); 59 | }; 60 | -------------------------------------------------------------------------------- /src/panel-triggers/components/Problems/AckCell.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from '@emotion/css'; 3 | import { RTCell } from '../../types'; 4 | import { ProblemDTO } from '../../../datasource/types'; 5 | import { FAIcon } from '../../../components'; 6 | import { useTheme, stylesFactory } from '@grafana/ui'; 7 | import { GrafanaTheme } from '@grafana/data'; 8 | 9 | const getStyles = stylesFactory((theme: GrafanaTheme) => { 10 | return { 11 | countLabel: css` 12 | font-size: ${theme.typography.size.sm}; 13 | `, 14 | }; 15 | }); 16 | 17 | export const AckCell: React.FC> = (props: RTCell) => { 18 | const problem = props.original; 19 | const theme = useTheme(); 20 | const styles = getStyles(theme); 21 | 22 | return ( 23 |
24 | {problem.acknowledges?.length > 0 && ( 25 | <> 26 | 27 | ({problem.acknowledges?.length}) 28 | 29 | )} 30 |
31 | ); 32 | }; 33 | 34 | export default AckCell; 35 | -------------------------------------------------------------------------------- /src/panel-triggers/components/Problems/AcknowledgesList.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { ZBXAcknowledge } from '../../../datasource/types'; 3 | 4 | interface AcknowledgesListProps { 5 | acknowledges: ZBXAcknowledge[]; 6 | } 7 | 8 | export default function AcknowledgesList(props: AcknowledgesListProps) { 9 | const { acknowledges } = props; 10 | return ( 11 |
12 |
13 | {acknowledges.map((ack) => ( 14 | 15 | {ack.time} 16 | 17 | ))} 18 |
19 |
20 | {acknowledges.map((ack) => ( 21 | 22 | {formatUserName(ack)} 23 | 24 | ))} 25 |
26 |
27 | {acknowledges.map((ack) => ( 28 | 29 | {ack.message} 30 | 31 | ))} 32 |
33 |
34 | ); 35 | } 36 | 37 | function formatUserName(ack: ZBXAcknowledge): string { 38 | if (!ack.name && !ack.surname) { 39 | return ack.user; 40 | } else { 41 | return `${ack.name} ${ack.surname}`.trim(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/panel-triggers/components/Problems/ProblemExpression.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from '@emotion/css'; 3 | import { Tooltip, useStyles2 } from '@grafana/ui'; 4 | import { GrafanaTheme2 } from '@grafana/data'; 5 | import { ProblemDTO } from '../../../datasource/types'; 6 | 7 | interface Props { 8 | problem: ProblemDTO; 9 | } 10 | 11 | export const ProblemExpression = ({ problem }: Props) => { 12 | const styles = useStyles2(getStyles); 13 | return ( 14 | <> 15 | 16 | Expression:  17 | 18 | {problem.expression} 19 | 20 | ); 21 | }; 22 | 23 | const getStyles = (theme: GrafanaTheme2) => ({ 24 | label: css` 25 | color: ${theme.colors.text.secondary}; 26 | `, 27 | expression: css` 28 | font-family: ${theme.typography.fontFamilyMonospace}; 29 | `, 30 | }); 31 | -------------------------------------------------------------------------------- /src/panel-triggers/components/Problems/ProblemGroups.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from '@emotion/css'; 3 | import { useStyles2 } from '@grafana/ui'; 4 | import { GrafanaTheme2 } from '@grafana/data'; 5 | import { FAIcon } from '../../../components'; 6 | import { ZBXGroup } from '../../../datasource/types'; 7 | 8 | interface ProblemGroupsProps { 9 | groups: ZBXGroup[]; 10 | className?: string; 11 | } 12 | 13 | export const ProblemGroups = ({ groups }: ProblemGroupsProps) => { 14 | const styles = useStyles2(getStyles); 15 | return ( 16 | <> 17 | {groups.map((g) => ( 18 |
19 | 20 | {g.name} 21 |
22 | ))} 23 | 24 | ); 25 | }; 26 | 27 | const getStyles = (theme: GrafanaTheme2) => ({ 28 | groupContainer: css` 29 | margin-bottom: ${theme.spacing(0.2)}; 30 | `, 31 | }); 32 | -------------------------------------------------------------------------------- /src/panel-triggers/components/Problems/ProblemHosts.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from '@emotion/css'; 3 | import { useStyles2 } from '@grafana/ui'; 4 | import { GrafanaTheme2 } from '@grafana/data'; 5 | import { FAIcon } from '../../../components'; 6 | import { ZBXHost } from '../../../datasource/types'; 7 | 8 | interface ProblemHostsProps { 9 | hosts: ZBXHost[]; 10 | className?: string; 11 | } 12 | 13 | export const ProblemHosts = ({ hosts }: ProblemHostsProps) => { 14 | const styles = useStyles2(getStyles); 15 | return ( 16 | <> 17 | {hosts.map((h) => ( 18 |
19 | 20 | {h.name} 21 |
22 | ))} 23 | 24 | ); 25 | }; 26 | 27 | export const ProblemHostsDescription = ({ hosts }: ProblemHostsProps) => { 28 | const styles = useStyles2(getStyles); 29 | return ( 30 | <> 31 | Host description:  32 | {hosts.map((h, i) => ( 33 | {h.description} 34 | ))} 35 | 36 | ); 37 | }; 38 | 39 | const getStyles = (theme: GrafanaTheme2) => ({ 40 | hostContainer: css` 41 | margin-bottom: ${theme.spacing(0.2)}; 42 | `, 43 | label: css` 44 | color: ${theme.colors.text.secondary}; 45 | `, 46 | }); 47 | -------------------------------------------------------------------------------- /src/panel-triggers/components/Problems/ProblemItems.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { css } from '@emotion/css'; 3 | import { Tooltip, useStyles2 } from '@grafana/ui'; 4 | import { GrafanaTheme2 } from '@grafana/data'; 5 | import { FAIcon } from '../../../components'; 6 | import { expandItemName } from '../../../datasource/utils'; 7 | import { ZBXItem } from '../../../datasource/types'; 8 | 9 | interface ProblemItemsProps { 10 | items: ZBXItem[]; 11 | } 12 | 13 | export const ProblemItems = ({ items }: ProblemItemsProps) => { 14 | const styles = useStyles2(getStyles); 15 | return ( 16 |
17 | {items.length > 1 ? ( 18 | items.map((item) => ) 19 | ) : ( 20 | 21 | )} 22 |
23 | ); 24 | }; 25 | 26 | interface ProblemItemProps { 27 | item: ZBXItem; 28 | showName?: boolean; 29 | } 30 | 31 | const ProblemItem = ({ item, showName }: ProblemItemProps) => { 32 | const styles = useStyles2(getStyles); 33 | const itemName = expandItemName(item.name, item.key_); 34 | const tooltipContent = () => ( 35 | <> 36 | {itemName} 37 |
38 | {item.lastvalue} 39 | 40 | ); 41 | 42 | return ( 43 |
44 | 45 | {showName && {item.name}: } 46 | 47 | {item.lastvalue} 48 | 49 |
50 | ); 51 | }; 52 | 53 | const getStyles = (theme: GrafanaTheme2) => ({ 54 | itemContainer: css` 55 | display: flex; 56 | `, 57 | itemName: css` 58 | color: ${theme.colors.text.secondary}; 59 | `, 60 | itemsRow: css` 61 | overflow: hidden; 62 | `, 63 | }); 64 | -------------------------------------------------------------------------------- /src/panel-triggers/components/Problems/ProblemStatusBar.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Tooltip } from '@grafana/ui'; 3 | import FAIcon from '../../../components/FAIcon/FAIcon'; 4 | import { ZBXAlert, ProblemDTO } from '../../../datasource/types'; 5 | 6 | export interface ProblemStatusBarProps { 7 | problem: ProblemDTO; 8 | alerts?: ZBXAlert[]; 9 | className?: string; 10 | } 11 | 12 | export default function ProblemStatusBar(props: ProblemStatusBarProps) { 13 | const { problem, alerts, className } = props; 14 | const multiEvent = problem.type === '1'; 15 | const link = problem.url && problem.url !== ''; 16 | const maintenance = problem.maintenance; 17 | const manualClose = problem.manual_close === '1'; 18 | const error = problem.error && problem.error !== ''; 19 | const stateUnknown = problem.state === '1'; 20 | const closeByTag = problem.correlation_mode === '1'; 21 | const actions = alerts && alerts.length !== 0; 22 | const actionMessage = actions ? alerts[0].message : ''; 23 | 24 | return ( 25 |
26 | 27 | 28 | 29 | 34 | 35 | 36 | 37 | 38 |
39 | ); 40 | } 41 | 42 | interface ProblemStatusBarItemProps { 43 | icon: string; 44 | fired?: boolean; 45 | link?: string; 46 | tooltip?: string; 47 | } 48 | 49 | function ProblemStatusBarItem(props: ProblemStatusBarItemProps) { 50 | const { fired, icon, link, tooltip } = props; 51 | let item = ( 52 |
53 | 54 |
55 | ); 56 | if (tooltip && fired) { 57 | item = ( 58 | 59 | {item} 60 | 61 | ); 62 | } 63 | return link ? ( 64 | 65 | {item} 66 | 67 | ) : ( 68 | item 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/panel-triggers/components/ResetColumnsEditor.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Button } from '@grafana/ui'; 3 | import { StandardEditorProps } from '@grafana/data'; 4 | 5 | export const ResetColumnsEditor = ({ onChange }: StandardEditorProps) => { 6 | return ( 7 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /src/panel-triggers/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "panel", 3 | "name": "Zabbix Problems", 4 | "id": "alexanderzobnin-zabbix-triggers-panel", 5 | "info": { 6 | "author": { 7 | "name": "Grafana Labs", 8 | "url": "https://grafana.com" 9 | }, 10 | "logos": { 11 | "small": "img/icn-zabbix-problems-panel.svg", 12 | "large": "img/icn-zabbix-problems-panel.svg" 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/panel-triggers/utils.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { dateMath } from '@grafana/data'; 3 | import { DataQuery } from '@grafana/schema'; 4 | import * as utils from '../datasource/utils'; 5 | import { ProblemDTO } from 'datasource/types'; 6 | 7 | export function isNewProblem(problem: ProblemDTO, highlightNewerThan: string): boolean { 8 | try { 9 | const highlightIntervalMs = utils.parseInterval(highlightNewerThan); 10 | const durationSec = Date.now() - problem.timestamp * 1000; 11 | return durationSec < highlightIntervalMs; 12 | } catch (e) { 13 | return false; 14 | } 15 | } 16 | 17 | const DEFAULT_TIME_FORMAT = 'DD MMM YYYY HH:mm:ss'; 18 | 19 | export function formatLastChange(lastchangeUnix: number, customFormat?: string) { 20 | const date = new Date(lastchangeUnix * 1000); 21 | const timestamp = dateMath.parse(date); 22 | const format = customFormat || DEFAULT_TIME_FORMAT; 23 | const lastchange = timestamp!.format(format); 24 | return lastchange; 25 | } 26 | 27 | export const getNextRefIdChar = (queries: DataQuery[]): string => { 28 | const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; 29 | const nextLetter = _.find(letters, (refId) => { 30 | return _.every(queries, (other) => { 31 | return other.refId !== refId; 32 | }); 33 | }); 34 | return nextLetter || 'A'; 35 | }; 36 | -------------------------------------------------------------------------------- /src/plugin.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "app", 3 | "name": "Zabbix", 4 | "id": "alexanderzobnin-zabbix-app", 5 | "info": { 6 | "description": "Zabbix plugin for Grafana", 7 | "author": { 8 | "name": "Grafana Labs", 9 | "url": "https://grafana.com" 10 | }, 11 | "keywords": ["zabbix", "servers and vms"], 12 | "logos": { 13 | "small": "img/icn-zabbix-app.svg", 14 | "large": "img/icn-zabbix-app.svg" 15 | }, 16 | "links": [ 17 | { 18 | "name": "GitHub", 19 | "url": "https://github.com/grafana/grafana-zabbix" 20 | }, 21 | { 22 | "name": "Docs", 23 | "url": "https://grafana.com/docs/plugins/alexanderzobnin-zabbix-app/latest/" 24 | }, 25 | { 26 | "name": "License", 27 | "url": "https://github.com/grafana/grafana-zabbix/blob/main/LICENSE" 28 | } 29 | ], 30 | "screenshots": [ 31 | { 32 | "name": "Showcase", 33 | "path": "img/screenshot-showcase.png" 34 | }, 35 | { 36 | "name": "Dashboard", 37 | "path": "img/screenshot-dashboard01.png" 38 | }, 39 | { 40 | "name": "Annotations", 41 | "path": "img/screenshot-annotations.png" 42 | }, 43 | { 44 | "name": "Metric Editor", 45 | "path": "img/screenshot-metric_editor.png" 46 | }, 47 | { 48 | "name": "Triggers", 49 | "path": "img/screenshot-triggers.png" 50 | } 51 | ], 52 | "version": "%VERSION%", 53 | "updated": "%TODAY%" 54 | }, 55 | "includes": [ 56 | { 57 | "type": "datasource", 58 | "name": "Zabbix data source", 59 | "path": "datasource/plugin.json" 60 | }, 61 | { 62 | "type": "panel", 63 | "name": "Problems panel", 64 | "path": "panel-triggers/plugin.json" 65 | } 66 | ], 67 | "dependencies": { 68 | "grafanaDependency": ">=10.4.8", 69 | "grafanaVersion": "10.4", 70 | "plugins": [] 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/styles/_query_editor.scss: -------------------------------------------------------------------------------- 1 | .zbx-regex { 2 | color: $regex; 3 | } 4 | 5 | .zbx-variable { 6 | color: $variable; 7 | } 8 | -------------------------------------------------------------------------------- /src/styles/_react-table.scss: -------------------------------------------------------------------------------- 1 | // ReactTable basic overrides (does not include pivot/groups/filters) 2 | 3 | .ReactTable { 4 | border: none; 5 | } 6 | 7 | .ReactTable .rt-table { 8 | // Allow some space for the no-data text 9 | min-height: 90px; 10 | } 11 | 12 | .ReactTable .rt-thead.-header { 13 | box-shadow: none; 14 | background: $list-item-bg; 15 | border-top: 2px solid $body-bg; 16 | border-bottom: 2px solid $body-bg; 17 | height: 2em; 18 | } 19 | .ReactTable .rt-thead.-header .rt-th { 20 | text-align: left; 21 | color: $blue; 22 | font-weight: 500; 23 | } 24 | .ReactTable .rt-thead .rt-td, 25 | .ReactTable .rt-thead .rt-th { 26 | padding: 0.45em 0 0.45em 1.1em; 27 | border-right: none; 28 | box-shadow: none; 29 | } 30 | .ReactTable .rt-tbody .rt-td { 31 | padding: 0.45em 0 0.45em 1.1em; 32 | border-bottom: 2px solid $body-bg; 33 | border-right: 2px solid $body-bg; 34 | } 35 | .ReactTable .rt-tbody .rt-td:last-child { 36 | border-right: none; 37 | } 38 | .ReactTable .-pagination { 39 | border-top: none; 40 | box-shadow: none; 41 | margin-top: $panel-margin; 42 | } 43 | .ReactTable .-pagination .-btn { 44 | color: $blue; 45 | background: $list-item-bg; 46 | } 47 | .ReactTable .-pagination input, 48 | .ReactTable .-pagination select { 49 | color: $input-color; 50 | background-color: $input-bg; 51 | } 52 | .ReactTable .-loading { 53 | background: $input-bg; 54 | } 55 | .ReactTable .-loading.-active { 56 | opacity: 0.8; 57 | } 58 | .ReactTable .-loading > div { 59 | color: $input-color; 60 | } 61 | .ReactTable .rt-tr .rt-td:last-child { 62 | text-align: right; 63 | } 64 | .ReactTable .rt-noData { 65 | top: 60px; 66 | z-index: inherit; 67 | } 68 | -------------------------------------------------------------------------------- /src/styles/_variables.dark.scss: -------------------------------------------------------------------------------- 1 | $regex: #d69e2e; 2 | 3 | $zbx-tag-color: $gray-5; 4 | $zbx-text-highlighted: $white; 5 | $zbx-text-color-disabled: $gray-1; 6 | $zbx-card-background-start: rgba(46, 46, 49, 0.1); 7 | $zbx-card-background-stop: rgba(38, 38, 40, 0.8); 8 | 9 | $action-button-color: $blue-dark; 10 | $action-button-text-color: $gray-4; 11 | 12 | $problems-border-color: #33b5e554; 13 | $problems-table-stripe: $dark-3; 14 | $problems-table-row-hovered: lighten($problems-table-stripe, 4%); 15 | $problems-table-row-hovered-shadow-color: rgba($blue, 0.5); 16 | $problems-table-row-hovered-shadow: 0px 0px 8px $problems-table-row-hovered-shadow-color; 17 | $problem-details-background: $dark-3; 18 | 19 | $problem-expander-highlighted-background: #393939; 20 | $problem-expander-highlighted-color: $blue; 21 | $problem-expander-expanded-color: $blue; 22 | 23 | $problem-statusbar-background: $dark-2; 24 | $problem-statusbar-muted: $dark-3; 25 | $problem-statusbar-fired: $orange; 26 | $problem-statusbar-glow: 0px 0px 10px rgba($problem-statusbar-fired, 0.1); 27 | 28 | $problem-event-highlighted: rgba($white, 0.7); 29 | $problem-event-core-highlighted: $white; 30 | $problem-event-core: #000000; 31 | $problem-event-ok-color: #38bd71; 32 | $problem-event-problem-color: #d70000; 33 | 34 | $problem-icon-problem-color: rgb(163, 16, 0); 35 | $problem-icon-ok-color: #629e51; 36 | 37 | $problem-container-shadow: $dark-1; 38 | $porblem-ack-shadow: -2px 2px 10px 1px $problem-container-shadow; 39 | 40 | $problems-footer-shadow: $problem-container-shadow; 41 | -------------------------------------------------------------------------------- /src/styles/_variables.light.scss: -------------------------------------------------------------------------------- 1 | $regex: #d69e2e; 2 | 3 | $zbx-tag-color: $gray-6; 4 | $zbx-text-highlighted: $black; 5 | $zbx-text-color-disabled: $gray-3; 6 | $zbx-card-background-start: rgba(233, 237, 242, 0.35); 7 | $zbx-card-background-stop: rgba(221, 228, 237, 1); 8 | 9 | $action-button-color: #497dc0; 10 | $action-button-text-color: $gray-6; 11 | 12 | $problems-border-color: #9d9d9d; 13 | $problems-table-stripe: $gray-6; 14 | $problems-table-row-hovered: #a2e0ef; 15 | $problems-table-row-hovered-shadow-color: $blue; 16 | $problems-table-row-hovered-shadow: 0px 1px 8px 1px $problems-table-row-hovered-shadow-color; 17 | $problem-details-background: $gray-6; 18 | 19 | $problem-expander-highlighted-background: #424755; 20 | $problem-expander-highlighted-color: #8ad9f5; 21 | $problem-expander-expanded-color: $blue; 22 | 23 | $problem-statusbar-background: $gray-4; 24 | $problem-statusbar-muted: $gray-5; 25 | $problem-statusbar-fired: #ca4c17; 26 | $problem-statusbar-glow: inset 0px 0px 10px rgba($problem-statusbar-fired, 0.5); 27 | 28 | $problem-event-highlighted: $white; 29 | $problem-event-core-highlighted: $white; 30 | $problem-event-core: $gray-6; 31 | $problem-event-ok-color: #2baf63; 32 | $problem-event-problem-color: #d70000; 33 | 34 | $problem-icon-problem-color: rgb(163, 16, 0); 35 | $problem-icon-ok-color: #629e51; 36 | 37 | $problem-container-shadow: $gray-3; 38 | $porblem-ack-shadow: -2px 2px 10px 0px $problem-container-shadow; 39 | 40 | $problems-footer-shadow: rgba($problem-container-shadow, 0.5); 41 | -------------------------------------------------------------------------------- /src/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | $panel-margin: 10px; 2 | -------------------------------------------------------------------------------- /src/styles/dark.scss: -------------------------------------------------------------------------------- 1 | @import 'grafana_variables.dark'; 2 | @import 'variables.dark'; 3 | @import 'grafana-zabbix'; 4 | -------------------------------------------------------------------------------- /src/styles/grafana-zabbix.scss: -------------------------------------------------------------------------------- 1 | // DEPENDENCIES 2 | @import '../../node_modules/react-table-6/react-table.css'; 3 | 4 | @import 'variables'; 5 | @import 'panel-triggers'; 6 | @import 'panel-problems'; 7 | @import 'query_editor'; 8 | @import 'react-table'; 9 | -------------------------------------------------------------------------------- /src/styles/light.scss: -------------------------------------------------------------------------------- 1 | @import 'grafana_variables.light'; 2 | @import 'variables.light'; 3 | @import 'grafana-zabbix'; 4 | -------------------------------------------------------------------------------- /src/test-setup/cssStub.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /src/test-setup/jest-setup.js: -------------------------------------------------------------------------------- 1 | import { PanelCtrl, MetricsPanelCtrl } from './panelStub'; 2 | 3 | jest.mock( 4 | 'grafana/app/features/templating/template_srv', 5 | () => { 6 | return {}; 7 | }, 8 | { virtual: true } 9 | ); 10 | 11 | jest.mock( 12 | 'grafana/app/features/dashboard/dashboard_srv', 13 | () => { 14 | return {}; 15 | }, 16 | { virtual: true } 17 | ); 18 | 19 | jest.mock( 20 | '@grafana/runtime', 21 | () => { 22 | return { 23 | getBackendSrv: () => ({ 24 | datasourceRequest: jest.fn().mockResolvedValue(), 25 | }), 26 | getTemplateSrv: () => ({ 27 | replace: jest.fn().mockImplementation((query) => query), 28 | }), 29 | }; 30 | }, 31 | { virtual: true } 32 | ); 33 | 34 | jest.mock( 35 | 'grafana/app/core/core_module', 36 | () => { 37 | return { 38 | directive: function () {}, 39 | }; 40 | }, 41 | { virtual: true } 42 | ); 43 | 44 | jest.mock( 45 | 'grafana/app/core/core', 46 | () => ({ 47 | contextSrv: {}, 48 | }), 49 | { virtual: true } 50 | ); 51 | 52 | const mockPanelCtrl = PanelCtrl; 53 | const mockMetricsPanelCtrl = MetricsPanelCtrl; 54 | 55 | jest.mock( 56 | 'grafana/app/plugins/sdk', 57 | () => { 58 | return { 59 | QueryCtrl: null, 60 | PanelCtrl: mockPanelCtrl, 61 | loadPluginCss: () => {}, 62 | PanelCtrl: mockPanelCtrl, 63 | MetricsPanelCtrl: mockMetricsPanelCtrl, 64 | }; 65 | }, 66 | { virtual: true } 67 | ); 68 | 69 | jest.mock( 70 | 'grafana/app/core/utils/datemath', 71 | () => { 72 | const datemath = require('./modules/datemath'); 73 | return { 74 | parse: datemath.parse, 75 | parseDateMath: datemath.parseDateMath, 76 | isValid: datemath.isValid, 77 | }; 78 | }, 79 | { virtual: true } 80 | ); 81 | 82 | jest.mock( 83 | 'grafana/app/core/table_model', 84 | () => { 85 | return class TableModel { 86 | constructor() { 87 | this.columns = []; 88 | this.columnMap = {}; 89 | this.rows = []; 90 | this.type = 'table'; 91 | } 92 | 93 | addColumn(col) { 94 | if (!this.columnMap[col.text]) { 95 | this.columns.push(col); 96 | this.columnMap[col.text] = col; 97 | } 98 | } 99 | }; 100 | }, 101 | { virtual: true } 102 | ); 103 | 104 | jest.mock( 105 | 'grafana/app/core/config', 106 | () => { 107 | return { 108 | buildInfo: { env: 'development' }, 109 | }; 110 | }, 111 | { virtual: true } 112 | ); 113 | 114 | jest.mock( 115 | 'grafana/app/core/utils/kbn', 116 | () => { 117 | return { 118 | round_interval: (n) => n, 119 | secondsToHms: (n) => n + 'ms', 120 | }; 121 | }, 122 | { virtual: true } 123 | ); 124 | -------------------------------------------------------------------------------- /src/test-setup/mocks.ts: -------------------------------------------------------------------------------- 1 | export const templateSrvMock = { 2 | replace: jest.fn().mockImplementation((query) => query), 3 | }; 4 | 5 | export const backendSrvMock = { 6 | datasourceRequest: jest.fn(), 7 | }; 8 | 9 | export const datasourceSrvMock = { 10 | loadDatasource: jest.fn(), 11 | getAll: jest.fn(), 12 | }; 13 | 14 | export const timeSrvMock = { 15 | timeRange: jest.fn().mockReturnValue({ from: '', to: '' }), 16 | }; 17 | 18 | const defaultExports = { 19 | templateSrvMock, 20 | backendSrvMock, 21 | datasourceSrvMock, 22 | timeSrvMock, 23 | }; 24 | 25 | export default defaultExports; 26 | -------------------------------------------------------------------------------- /src/test-setup/modules/datemath.js: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import moment from 'moment'; 3 | 4 | var units = ['y', 'M', 'w', 'd', 'h', 'm', 's']; 5 | 6 | export function parse(text, roundUp) { 7 | if (!text) { return undefined; } 8 | if (moment.isMoment(text)) { return text; } 9 | if (_.isDate(text)) { return moment(text); } 10 | 11 | var time; 12 | var mathString = ''; 13 | var index; 14 | var parseString; 15 | 16 | if (text.substring(0, 3) === 'now') { 17 | time = moment(); 18 | mathString = text.substring('now'.length); 19 | } else { 20 | index = text.indexOf('||'); 21 | if (index === -1) { 22 | parseString = text; 23 | mathString = ''; // nothing else 24 | } else { 25 | parseString = text.substring(0, index); 26 | mathString = text.substring(index + 2); 27 | } 28 | // We're going to just require ISO8601 timestamps, k? 29 | time = moment(parseString, moment.ISO_8601); 30 | } 31 | 32 | if (!mathString.length) { 33 | return time; 34 | } 35 | 36 | return parseDateMath(mathString, time, roundUp); 37 | } 38 | 39 | export function isValid(text) { 40 | var date = parse(text); 41 | if (!date) { 42 | return false; 43 | } 44 | 45 | if (moment.isMoment(date)) { 46 | return date.isValid(); 47 | } 48 | 49 | return false; 50 | } 51 | 52 | export function parseDateMath(mathString, time, roundUp) { 53 | var dateTime = time; 54 | var i = 0; 55 | var len = mathString.length; 56 | 57 | while (i < len) { 58 | var c = mathString.charAt(i++); 59 | var type; 60 | var num; 61 | var unit; 62 | 63 | if (c === '/') { 64 | type = 0; 65 | } else if (c === '+') { 66 | type = 1; 67 | } else if (c === '-') { 68 | type = 2; 69 | } else { 70 | return undefined; 71 | } 72 | 73 | if (isNaN(mathString.charAt(i))) { 74 | num = 1; 75 | } else if (mathString.length === 2) { 76 | num = mathString.charAt(i); 77 | } else { 78 | var numFrom = i; 79 | while (!isNaN(mathString.charAt(i))) { 80 | i++; 81 | if (i > 10) { return undefined; } 82 | } 83 | num = parseInt(mathString.substring(numFrom, i), 10); 84 | } 85 | 86 | if (type === 0) { 87 | // rounding is only allowed on whole, single, units (eg M or 1M, not 0.5M or 2M) 88 | if (num !== 1) { 89 | return undefined; 90 | } 91 | } 92 | unit = mathString.charAt(i++); 93 | 94 | if (!_.includes(units, unit)) { 95 | return undefined; 96 | } else { 97 | if (type === 0) { 98 | if (roundUp) { 99 | dateTime.endOf(unit); 100 | } else { 101 | dateTime.startOf(unit); 102 | } 103 | } else if (type === 1) { 104 | dateTime.add(num, unit); 105 | } else if (type === 2) { 106 | dateTime.subtract(num, unit); 107 | } 108 | } 109 | } 110 | return dateTime; 111 | } 112 | -------------------------------------------------------------------------------- /tests/e2e/smoke.test.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@grafana/plugin-e2e'; 2 | 3 | test('Smoke test: plugin loads', async ({ createDataSourceConfigPage, page }) => { 4 | await createDataSourceConfigPage({ type: 'alexanderzobnin-zabbix-datasource' }); 5 | 6 | await expect(await page.getByText('Type: Zabbix', { exact: true })).toBeVisible(); 7 | await expect(await page.getByRole('heading', { name: 'Connection', exact: true })).toBeVisible(); 8 | }); 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.config/tsconfig.json", 3 | "compilerOptions": { 4 | // Disable some default rules from @grafana/tsconfig 5 | "noImplicitAny": false, 6 | "noImplicitThis": false, 7 | "strictPropertyInitialization": false, 8 | "strictFunctionTypes": false, 9 | "strictNullChecks": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /webpack.config.ts: -------------------------------------------------------------------------------- 1 | import type { Configuration } from 'webpack'; 2 | import { merge } from 'webpack-merge'; 3 | import grafanaConfig from './.config/webpack/webpack.config'; 4 | import MiniCssExtractPlugin from 'mini-css-extract-plugin'; 5 | import RemoveEmptyScriptsPlugin from 'webpack-remove-empty-scripts'; 6 | 7 | const config = async (env): Promise => { 8 | const baseConfig = await grafanaConfig(env); 9 | 10 | return merge(baseConfig, { 11 | // Add custom config here... 12 | entry: { 13 | module: './module.ts', 14 | 'datasource/module': './datasource/module.ts', 15 | 'panel-triggers/module': './panel-triggers/module.tsx', 16 | dark: './styles/dark.scss', 17 | light: './styles/light.scss', 18 | }, 19 | 20 | module: { 21 | rules: [ 22 | { 23 | test: /(dark|light)\.scss$/, 24 | exclude: /node_modules/, 25 | use: [ 26 | MiniCssExtractPlugin.loader, 27 | { 28 | loader: 'css-loader', 29 | options: { 30 | importLoaders: 1, 31 | url: false, 32 | sourceMap: false, 33 | }, 34 | }, 35 | { 36 | loader: require.resolve('postcss-loader'), 37 | options: { 38 | postcssOptions: { 39 | plugins: () => [ 40 | require('postcss-flexbugs-fixes'), 41 | require('postcss-preset-env')({ 42 | autoprefixer: { flexbox: 'no-2009', grid: true }, 43 | }), 44 | ], 45 | }, 46 | }, 47 | }, 48 | { 49 | loader: 'sass-loader', 50 | options: { 51 | sourceMap: false, 52 | }, 53 | }, 54 | ], 55 | }, 56 | ], 57 | }, 58 | 59 | plugins: [ 60 | new RemoveEmptyScriptsPlugin({}), 61 | new MiniCssExtractPlugin({ 62 | filename: 'styles/[name].css', 63 | }), 64 | ], 65 | }); 66 | }; 67 | 68 | export default config; 69 | --------------------------------------------------------------------------------