Обычно, для отображения информации с веб-сервера данные загружают в систему мониторинга, а затем передают в Grafana. О том, как сделать это напрямую и о некоторых нюансах на пути к цели — под катом.

Разработка плагинов для Grafana ведется на JavaScript (es6) или TypeScript и подразумевает использование Node.js совместно с каким либо сборщиком, напр. grunt.
Первым делом создаем папку проекта, куда добавляем файлы package.json, gruntfile.js и другие.
После того, как package.json создан, можно установить все необходимые для разработки зависимости и сборщик, выполнив в папке проекта
В результате будет создана папка node_modules, содержащая примерно 50мб вспомогательных файлов, и станет доступна команда
Далее создаем создаем папку src с необходимой структурой. В файле plugin.json задаем
Стоит отметить, что плагин может реализовывать не только источник данных или новый тип панели, но и группу решений. В этом случае plugin.json будет иметь отличную структуру.
В папку /src/partials добавляем файл config.html, содержащий блок, отображаемый при подключении к источнику. Обычно стандартного для http — достаточно.
В некоторых плагинах можно встретить query.options.html, содержащий настройки для метрик. С версии 4.5 данные настройки считываются из plugin.json.
Следующий файл — query.editor.html реализует как будут задаваться метрики (строки в интерфейсе). Обычно в них используются выпадающие списки, а не просто поле ввода. Для Angular элемент со списком, связываемый с переменной
В случае, если список значений, содержащийся в
Обратите внимание, что
Помимо
Отмечу, что:
1. Последний элемент с классом
2. Вы можете добавлять/скрывать элементы в строке метрики в зависимости от типа панели посредством условного отображения
Файлы module.js и query_ctrl.js достаточно просты, и могут быть написаны по аналогии с другими источниками данных, напр. Simple Json. Основная логика располагается в datasource.js.
Класс, описываемый в этом модуле, должен реализовывать как минимум два метода
Из приведенного примера легко видеть, что данные для всех метрик запрашиваются одновременно. Основные поля —
Список
Для одной метрики ответом может быть несколько результатов, напр. несколько графиков. Их можно складывать в общий массив, который потом пойдет в итоговый набор для отображения, где уже не важно для к��кой метрики был получен тот или иной результат.
Формат данных, отдаваемый
В Simple Json выбор формата предлагается решать дополнительным атрибутом метрики, что не очень хорошо.

Поскольку можно делать это автоматически, добавив в
Результатом метода
Для запроса данных используется метод

В случае браузера опрашиваемый веб-сервер должен поддерживать CORS.
Если для получения результата для всех метрик необходимо выполнить несколько запросов к источнику, то можно воспользоваться
Для того, чтобы источник данных поддерживал переменные, нужно реализовать метод
Для аннотаций требуется реализация
Для установки достаточно скопировать плагин в папку %GRAFANA_PATH%/data/plugins для Windows или /var/lib/grafana/plugins для остальных систем и перезапустить Grafana.
Если вы хотите, чтобы ваш плагин добавили в список доступных, то надо сделать pull request в репозитарий плагинов или обратиться к разработчикам посредством форума.

Disclaimer
Из-за нежелания автора углубляться в изучение устаревшего AngularJS, используемого Grafana для интерфейса, и практически полного отсутствия документации по разработке плагинов, данная статья может содержать неверные высказывания, следы арахиса и других орехов.
Подготовка
Разработка плагинов для Grafana ведется на JavaScript (es6) или TypeScript и подразумевает использование Node.js совместно с каким либо сборщиком, напр. grunt.
Типичное содержимое папки проекта
/dist ... // Дистрибутив плагина. Grafana использует только эту папку. /src /img logo.svg // иконка, в любом формате /partials // допол��ительные шаблоны config.html // настройки для подключения. Стандартный, http. query.editor.html // отображение строк метрик при редактировании. Ключевой. datasource.js // Класс реализующий получение данных из источника module.js // Точка входа в плагин plugin.json // Мета-данные плагина query_ctrl.js // Класс, связывающий html-шаблоны и данные README.md // Описание, отображаемое при просмотре деталей плагина в Grafana gruntfile.js // Набор команд для сборщика LICENSE.txt // Лицензия package.json // Мета-данные Node.js проекта, включающего зависимости README.md // Описание Node.js проекта
Первым делом создаем папку проекта, куда добавляем файлы package.json, gruntfile.js и другие.
Примерное содержимое package.json
{ "name": "имя-проекта", "version": "0.1.0", "description": "короткое-описание-проекта", "repository": { "type": "git", "url": "git+https://ссылка-github-репозитария" }, "author": "ваше-имя", "license": "MIT", "devDependencies": { "babel": "~6.5.1", "grunt": "~0.4.5", "grunt-babel": "~6.0.0", "grunt-contrib-clean": "~0.6.0", "grunt-contrib-copy": "~0.8.2", "grunt-contrib-uglify": "~0.11.0", "grunt-contrib-watch": "^0.6.1", "grunt-execute": "~0.2.2", "grunt-sass": "^1.1.0", "grunt-systemjs-builder": "^0.2.5", "load-grunt-tasks": "~3.2.0", "babel-plugin-transform-es2015-for-of": "^6.6.0", "babel-plugin-transform-es2015-modules-systemjs": "^6.24.1", "babel-preset-es2015": "^6.24.1" }, "dependencies": {}, "homepage": "https://домашняя-страница-проекта" }
Примерное содержимое gruntfile.js
module.exports = function(grunt) { require('load-grunt-tasks')(grunt); grunt.loadNpmTasks('grunt-execute'); grunt.loadNpmTasks('grunt-contrib-clean'); grunt.loadNpmTasks('grunt-build-number'); grunt.initConfig({ clean: ["dist"], copy: { src_to_dist: { cwd: 'src', expand: true, src: [ '**/*', '!*.js', '!module.js', '!**/*.scss' ], dest: 'dist/' }, pluginDef: { expand: true, src: ['plugin.json'], dest: 'dist/', } }, watch: { rebuild_all: { files: ['src/**/*', 'plugin.json'], tasks: ['default'], options: {spawn: false} }, }, babel: { options: { sourceMap: true, presets: ["es2015"], plugins: ['transform-es2015-modules-systemjs', "transform-es2015-for-of"], }, dist: { files: [{ cwd: 'src', expand: true, src: [ '*.js', 'module.js', ], dest: 'dist/' }] }, }, sass: { options: { sourceMap: true }, dist: { files: { } } } }); grunt.registerTask('default', [ 'clean', 'copy:src_to_dist', 'copy:pluginDef', 'babel', 'sass' ]); }
После того, как package.json создан, можно установить все необходимые для разработки зависимости и сборщик, выполнив в папке проекта
npm install --only=dev npm install grunt -g
В результате будет создана папка node_modules, содержащая примерно 50мб вспомогательных файлов, и станет доступна команда
grunt для сборки дистрибутива в папку dist.Далее создаем создаем папку src с необходимой структурой. В файле plugin.json задаем
id проекта как автор-источник-datasource, а также какую информацию он будет предоставлять, задавая значения переменных metrics, alerting и annotations. Подробнее о plugin.json — здесь. Примерное содержимое plugin.json
{ "name": "Имя-источника", "id": "Уникальный-идентификатор-плагина", "type": "datasource", "metrics": true, "alerting": false, "annotations": false, "info": { "description": "Краткое-описание", "author": { "name": "Ваше-имя", "url": "Ваш-сайт" }, "logos": { "small": "img/logo.svg", "large": "img/logo.svg" }, "links": [ { "name": "GitHub", "url": "https://ссылка-github-репозитария" }, { "name": "Лицензия", "url": "https://ссылка-на-файл-лицензии" } ], "version": "0.1.0", "updated": "2018-05-10" }, "dependencies": { "grafanaVersion": "5.x", "plugins": [] } }
Стоит отметить, что плагин может реализовывать не только источник данных или новый тип панели, но и группу решений. В этом случае plugin.json будет иметь отличную структуру.
html элементы
В папку /src/partials добавляем файл config.html, содержащий блок, отображаемый при подключении к источнику. Обычно стандартного для http — достаточно.
Содержимое config.html
<datasource-http-settings current="ctrl.current"></datasource-http-settings>
В некоторых плагинах можно встретить query.options.html, содержащий настройки для метрик. С версии 4.5 данные настройки считываются из plugin.json.
Следующий файл — query.editor.html реализует как будут задаваться метрики (строки в интерфейсе). Обычно в них используются выпадающие списки, а не просто поле ввода. Для Angular элемент со списком, связываемый с переменной
ctrl.target.myprop, выглядит так<select ng-model="ctrl.target.myprop" ng-options="v.value as v.name for v in ctrl.myprops"> </select>
В случае, если список значений, содержащийся в
ctrl.myprops, должен быть загружен асинхронно, то необходимо будет создать контроллер. В Grafana уже имеется компонент с нужной реализацией.<gf-form-dropdown model="ctrl.target.myprop" class = "max-width-12" lookup-text="true" allow-custom = "false" get-options = "ctrl.getMyProps()" on-change = "ctrl.panelCtrl.refresh()" > </gf-form-dropdown>
ctrl — это объект класса, реализуемого в query_ctrl.js, связанный с текущей метрикой.ctrl.target содержит свойства метрики, которые будут отправлены на источник в запросе.ctrl.panelCtrl.refresh() заставляет панель запросить данные заново.lookup-text задает доступна ли для поля подсказка выпадающим списком.allow-custom задает допустимо выбирать элементы не из выпадающего списка.get-options метод для получения элементов выпадающего списка. При этом результат метода, возвращаемый как значение или promise, должен быть массивом элементов вида {text: "текст", value: "значение"}.Обратите внимание, что
model, get-options и on-change отличаются от исходных ng-model, ng-options и ng-change.Помимо
gf-form-dropdown еще есть metric-segment-model. Его использование можно увидеть здесь. Документации на компоненты — нет, поэтому их список и возможности можно узнать только изучая исходники.Возможное содержимое query.editor.html
<query-editor-row query-ctrl="ctrl" class="mydatasource-datasource-query-row"> <div class="gf-form-inline"> <div class="gf-form max-width-12"> <gf-form-dropdown model="ctrl.target.myprop" class = "max-width-12" lookup-text="true" custom = "false" get-options="ctrl.getMyProps()" on-change = "ctrl.updateMyParams()" > </gf-form-dropdown> </div> <div class="gf-form" ng-if = "ctrl.panel.type == 'graph'"> <label class="gf-form-label width-5">Name</label> <input type="text" ng-model="ctrl.target.label" class="gf-form-input width-12" spellcheck="false" > </div> <div class="gf-form" ng-if = "ctrl.target.myparams.length > 0"> <label class="gf-form-label width-5">Params</label> <input type="text" ng-repeat = "param in ctrl.target.myparams" ng-model="ctrl.target.myparams[param]" class="gf-form-input width-12" spellcheck="false" placeholder = "{{param}}" ng-change = "ctrl.panelCtrl.refresh();" > </div> <div class="gf-form gf-form--grow"> <div class="gf-form-label gf-form-label--grow"></div> </div> </div> </query-editor-row>
Отмечу, что:
1. Последний элемент с классом
gf-form--grow нужен для заливки незанятой части строки ф��ном.2. Вы можете добавлять/скрывать элементы в строке метрики в зависимости от типа панели посредством условного отображения
ng-if = "ctrl.panel.type == 'graph'".Написание кода
Файлы module.js и query_ctrl.js достаточно просты, и могут быть написаны по аналогии с другими источниками данных, напр. Simple Json. Основная логика располагается в datasource.js.
Класс, описываемый в этом модуле, должен реализовывать как минимум два метода
testDatasource() и query(options). Первый используется для тестирования соединения с источником при его регистрации (кнопка «Save and Test»), второй вызывается каждый раз, когда панель запрашивает данные. Остановлюсь на нем подробнее.Пример options, передаваемого в метод query
{ "timezone":"browser", "panelId":6, "dashboardId":1, "range":{ "from":"2018-05-10T23:30:42.318Z", "to":"2018-05-10T23:47:11.566Z", "raw":{ "from":"2018-05-10T23:30:42.318Z", "to":"2018-05-10T23:47:11.566Z" } }, "rangeRaw":{ "from":"2018-05-10T23:30:42.318Z", "to":"2018-05-10T23:47:11.566Z" }, "interval":"2s", "intervalMs":2000, "targets":[ { "myprop":"value1", "myparams":{ "column":"val", "table":"t" }, "refId":"A", "$$hashKey":"object:174" }, { "refId":"B", "$$hashKey":"object:185", "myprop":"value2", "myparams":{ "column":"val2", "table":"t2" }, "datatype":"table" } ], "maxDataPoints":320, "scopedVars":{ "__interval":{ "text":"2s", "value":"2s" }, "__interval_ms":{ "text":2000, "value":2000 } } }
Из приведенного примера легко видеть, что данные для всех метрик запрашиваются одновременно. Основные поля —
range, содержащее период за который требуется информация, и targets — список метрик, каждой из которой соответствует свойство target у объекта класса, определяемого в query_ctrl. Список
targets необходимо отфильтровать по свойству hide, чтобы не запрашивать результаты «скрытых» метрик, а так же удалить заведомо «неправильные» метрики, например с неопределенными параметрами. Затем по полученному списку запрашиваются данные для каждой метрики и полученное необходимо преобразовать в формат поддерживаемый Grafana. Для одной метрики ответом может быть несколько результатов, напр. несколько графиков. Их можно складывать в общий массив, который потом пойдет в итоговый набор для отображения, где уже не важно для к��кой метрики был получен тот или иной результат.
Формат данных, отдаваемый
query, для разных типов панелей различен, так если данные запрошены для графика, то результат требуется преобразовать к виду {target: Имя-линии, datapoints: массив-точек-[значение, время]}, а для таблицы, то {columns: массив-вида-{text: Имя-колонки, type: Тип-данных-колонки}, rows: массив значений}. В Simple Json выбор формата предлагается решать дополнительным атрибутом метрики, что не очень хорошо.

Поскольку можно делать это автоматически, добавив в
target объекта атрибут type на основе this.panel.type и преобразовывать результат исходя из него. Несколько странно, что в options тип панели не передается.Результатом метода
query должен быть promise, возвращающий {data: массив-ответов}. Для запроса данных используется метод
backendSrv.datasourceRequest(options), который в зависимости от типа выбранного источника данных либо перенаправляет данные в Grafana или же выполняет запрос непосредственно браузером. 
В случае браузера опрашиваемый веб-сервер должен поддерживать CORS.
Если для получения результата для всех метрик необходимо выполнить несколько запросов к источнику, то можно воспользоваться
Promise.allvar requests = this.targets.map((target) => ... ); var scope = requests.map((req) => this.backendSrv.datasourceRequest(req)); return Promise.all(scope).then(function (results) { // results пр��образуем в data, согласно нужному типу ... return Promise.resolve({data}); })
Для того, чтобы источник данных поддерживал переменные, нужно реализовать метод
metricFindQuery(options), возвращающий массив (возможно через promise) с элементами вида {text: "текст", value: "значение"}. Кроме того, в query потребуется перебрать options.targets и для каждого элемента этого массива для всех его свойств, где может быть подставлена переменная, выполнить преобразованиеtarget.myprop = this.templateSrv.replace(target.myprop, options.scopedVars, 'regex');
Для аннотаций требуется реализация
annotationQuery(options).Установка и публикация
Для установки достаточно скопировать плагин в папку %GRAFANA_PATH%/data/plugins для Windows или /var/lib/grafana/plugins для остальных систем и перезапустить Grafana.
Если вы хотите, чтобы ваш плагин добавили в список доступных, то надо сделать pull request в репозитарий плагинов или обратиться к разработчикам посредством форума.
Ссылки
- Краткая официальная документация для разработчиков
- Официальный форум, раздел для разработчиков
- Исходники других источников данных
- Simple Json — самый простой для изучения
- IBM APM — чуть более сложный вариант
- Zabbix от Grafana — самый функциональный и сложный, эталон.
- PRTG — упрощенный аналог плагина для Zabbix.
- Httpsql — авторский источник данных для запросов к базам данных.
