Обычно, для отображения информации с веб-сервера данные загружают в систему мониторинга, а затем передают в 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 и другие.

Примерное содержимое 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.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 в репозитарий плагинов или обратиться к разработчикам посредством форума.

Ссылки