Обычно, для отображения информации с веб-сервера данные загружают в систему мониторинга, а затем передают в Grafana. О том, как сделать это напрямую и о некоторых нюансах на пути к цели — под катом.
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 и другие.
{
"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://домашняя-страница-проекта"
}
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 — здесь.
{
"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 — достаточно.
<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-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»), второй вызывается каждый раз, когда панель запрашивает данные. Остановлюсь на нем подробнее.
{
"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.all
var 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 — авторский источник данных для запросов к базам данных.
Автор: little-brother