Автор: Георгий Моисеев

В Tarantool перед нами стояла задача настроить мониторинг для клиентского проекта. Мы решили эту проблему с помощью grafonnet, библиотеки, которая позволяет писать информационные панели Grafana на языке Jsonnet. Эта библиотека избавила нас от лишних хлопот.

История разделена на две части. В первой части я расскажу о том, как мы начали работать с графоннетом. Я расскажу, почему мы выбрали этот инструмент и какие проблемы с его помощью решили. Вторая часть - это руководство по написанию простой панели управления Prometheus с использованием графоннета. Некоторые ситуации, описанные в первой части статьи, могут показаться вам знакомыми. В этом случае вторая часть поможет вам сделать первые шаги к решению этих проблем.

1. Примечания в скобках

1.1. Как я познакомился с графоннетом

Как обычно, все началось с проблемы.

В конце 2019 года наша команда запускала в производство два микросервиса кеширования. Одним из пунктов нашего контрольного списка была настроенная система мониторинга с предупреждениями об инцидентах. У клиента уже была полнофункциональная инфраструктура: Telegraf, InfluxDB и Grafana. После того, как мы настроили стандартные метрики и интегрировали Tarantool с локальными системами, нам пришлось представить десятки тысяч значений в виде готовых графиков. Мне было поручено найти решение.

Большинство людей в нашей команде начали работать с Tarantool примерно за год до событий. Хотя у нас был достаточно опыт в области разработки, мониторинг был для нас terra incognita. Новые метрики и изменения в существующих появлялись несколько раз в неделю. Мы разрабатывали панель управления Grafana итеративно - добавляя, удаляя и заменяя панели. Каждая итерация начиналась с творческого процесса, который сводился к изменению четырех информационных панелей вместо одной.

Одной из первоначальных целей было предоставление возможности отслеживать показатели для двух разных зон. Было два возможных решения. Запросы Grafana могут содержать динамические переменные, такие как значения из базы данных метрик или массивы констант. Переменные позволяют легко преобразовать информационную панель зоны A в информационную панель зоны B. К сожалению, этот удобный механизм конфликтует с не менее полезными алертами. Запросы оповещения панели не могут содержать переменные. По этой причине мы решили поддерживать две статические панели мониторинга для каждого проекта.

Мы также хотели оптимизировать наши рутинные задачи. Вначале единственная разница между двумя панелями мониторинга заключалась в их статических полях запроса, поэтому мы изначально использовали Ctrl + H для решения проблемы.

Grafana может экспортировать панели мониторинга в формате JSON. После внесения изменений в одну из панелей мониторинга мы загрузили ее и использовали текстовый редактор для замены значений переменных в запросах. Для более сложных замен мы написали скрипт Python, который считывает правила преобразования из файла YAML. Поскольку кеши имели отдельные механизмы для репликации Oracle и холодной загрузки данных, панели мониторинга в какой-то момент стали совершенно другими - с разными наборами графиков. Удалить панель с помощью скрипта легко, но добавить панель таким способом намного сложнее. Однажды мы увидели, что скрипт преобразования панели мониторинга становится инструментом для создания новых панелей мониторинга. Очевидно, кто-то другой уже нашел решение этой проблемы.

Среди таких решений были такие общедоступные проекты, как Weaveworks grafanalib, Генератор панелей Uber Grafana, Генератор панелей Showmax Grafana, графямл , и графоннет . Кроме того, Grafana из коробки позволяла создавать дашборды на JavaScript. Мы также рассмотрели возможность разработки собственного генератора приборной панели. В конечном итоге наша ситуация помогла нам сузить наш выбор до графоннета, единственного инструмента, который может отправлять запросы в InfluxDB.

1.2. Начало

Grafonnet - это проект Grafana с открытым исходным кодом для написания информационных панелей в виде кода (см. Страницу GitHub). Он включает в себя шаблоны для различных панелей Grafana и типов запросов, переменных и методов, чтобы объединить все это на панели инструментов.

Одним из основных преимуществ Grafonnet является то, что он использует Jsonnet - краткий и понятный язык . Поскольку вы обычно обрабатываете расширенные объекты JSON, которые поддерживают скрытые и функциональные поля, вы можете вручную завершить результат на любом этапе, не изменяя сам код.

Тем не менее, мы начали с настройки исходного кода графоннета. В панелях таблиц и статистики отсутствовали некоторые элементы, представленные в Grafana v6, а также не было шаблона для пользовательского расширения Панель состояния. Несмотря на поддержку запросов InfluxDB, grafonnet не позволял использовать блоки для указания запросов, работая только с сырым InfluxQL. Чтобы добавить панель на дашборд, вам пришлось вручную пересчитать координаты всех панелей ниже. Эта проблема также была решена с помощью сценария Jsonnet, который встроился в панель управления во время сборки панели и нашел подходящее место для каждой панели в зависимости от размера. Наши ключевые решения были переданы в Grafonnet через пул-реквесты и теперь доступны всем пользователям.

После того, как мы передали вышеупомянутые проекты в Grafonnet, наш клиент заказал нам еще один проект - универсальную панель инструментов для всех их Tarantool- на базе услуг. Используя код первой версии, нам удалось собрать универсальный шаблон, который также поддерживал расширения. Любой желающий может создавать собственные графики для конкретного проекта и добавлять их на свою панель управления во время сборки. Необработанный JSON потребует гораздо больше работы.

1.3. Набирать силу

Когда проект достиг определенного уровня готовности, мы решили создать производную - дашборд с открытым исходным кодом для стандартного приложения Tarantool Cartridge. Его можно найти среди официальных дашбордов Grafana и дашбордов сообщества: версия для InfluxDB, версия для Prometheus. Исходный код доступен на GitHub.

Изначально мы собирались разработать версию только для InfluxDB, так как у нас был опыт и уже проделанная работа. Однако наш пакет metrics поддерживает вывод показателей в популярном среди пользователей формате Prometheus. Это привело к тому, что мы разработали версию для Prometheus. Визуализация в панелях не зависит от запросов, что позволяет эффективно повторно использовать код. Более того, работа с готовыми шаблонами избавляет от лишних хлопот, когда приходится добавлять сразу много похожих панелей.

2. Графоннет на практике

2.1. Введение в Jsonnet

Нам понадобится Jsonnet v0.16.0. Реализация, используемая в проектах Tarantool, основана на Go (см. Страницу GitHub).

Во-первых, давайте обсудим ключевые моменты использования Jsonnet. На официальном сайте проекта есть подробное руководство и описание стандартной библиотеки функций.

Скрипт Jsonnet возвращает действительный JSON - строку, логическое значение, число, null, массив, или объект (словарь). Напишем базовый сценарий:

# script1.jsonnet
{
    field: 1 + 2
}

Используйте следующую команду для выполнения сценария:

jsonnet script1.jsonnet

Вы увидите результат в консоли:

{
   "field": 3
}

Чтобы сохранить вывод в файл, передайте флаг -o:

jsonnet script1.jsonnet -o result.json

Иногда удобно поместить результат в буфер обмена. Утилита xclip позволяет сделать это:

jsonnet script1.jsonnet | xclip -selection clipboard

Есть два типа сценариев Jsonnet - .libsonnet и .jsonnet. В то время как сценарии .libsonnet создают промежуточные структуры, сценарии .jsonnet используются для генерации конечных результатов.

Помимо типа JSON, Jsonnet поддерживает функции. Кроме того, объекты Jsonnet могут содержать скрытые поля, назначаемые с помощью оператора «::». Давайте создадим библиотеку математических утилит:

# math.libsonnet
{
    sum(a, b): a + b
}

Файлы могут быть связаны с помощью импорта. Теперь мы будем использовать нашу библиотеку в скрипте:

# script2.jsonnet
local math = import 'math.libsonnet';
{
    field: math.sum(1, 2)
}

Результатом будет объект, который мы уже видели выше.

В скрипте также могут быть локальные переменные:

# script3.jsonnet
local math = import 'math.libsonnet';
local value = math.sum(1, 2);
{
    field: value
}

Локальные переменные также могут быть объявлены внутри объектов:

# script4.jsonnet
local math = import 'math.libsonnet';
{
    local value = math.sum(1, 2),
    field: value
}

Эти переменные игнорируются конструкцией импорта и во время расчета конечного результата.

jsonnet script4.jsonnet
{
   "field": 3
}

2.2. Установка графоннета

Сначала установите зависимости с помощью jsonnet-bundler.

Затем инициализируйте свой проект с помощью следующей команды:

jb init

В вашем проекте будет создан файл с именем jsonnetfile.json. Добавьте к нему репозиторий grafonnet:

{
  "version": 1,
  "dependencies": [
    {
      "source": {
        "git": {
          "remote": "https://github.com/grafana/grafonnet-lib",
          "subdir": "grafonnet"
        }
      },
      "version": "master"
    }
  ],
  "legacyImports": true
}

Теперь выполните следующую команду:

jb install

Jsonnet-bundler загрузит репозитории и зарегистрирует коммиты в файле блокировки. Это удобно, если вы добавляете зависимость из определенной ветки:

{
  "version": 1,
  "dependencies": [
    {
      "source": {
        "git": {
          "remote": "https://github.com/grafana/grafonnet-lib.git",
          "subdir": "grafonnet"
        }
      },
      "version": "3082bfca110166cd69533fa3c0875fdb1b68c329",
      "sum": "4/sUV0Kk+o8I+wlYxL9R6EPhL/NiLfYHk+NXlU64RUk="
    }
  ],
  "legacyImports": false
}

При всех последующих вызовах jb install будет обращаться к файлу блокировки. Я рекомендую использовать для этого урока версию, указанную выше.

2.3. Создание дашборда

Как правило, дашборд имеет следующую структуру:

В своем руководстве я использую Grafana v6. Хотя панель управления работает с более поздними версиями Grafana, мы не будем использовать их новые функции.

2.3.1. Пустая панель управления

Во-первых, давайте создадим пустую панель инструментов:

# dashboard.jsonnet
local grafana = import 'grafonnet/grafana.libsonnet';
grafana.dashboard.new(
    title='My dashboard'
)

Чтобы скомпилировать приборную панель, запустите следующее:

jsonnet -J ./vendor dashboard.jsonnet -o dashboard.json

Флаг -J позволяет подключать внешние библиотеки. В нашем случае мы будем использовать каталог ./vendor, содержащий зависимости, установленные jsonnet-bundler.

2.3.2. Практика Docker-кластера

Я создал Docker-кластер, где можно попрактиковаться. Он содержит простое приложение Tarantool, Prometheus и Grafana и доступен как репозиторий GitHub. Клонируйте его и выполните команду ниже (требуется docker-compose):

docker-compose up

После запуска Grafana будет доступна по адресу localhost: 3000.

2.3.3. Импорт дашборда

Чтобы загрузить свою панель управления из JSON, используйте функцию импорта на боковой панели Grafana.

Загрузите файл или вставьте JSON из буфера обмена. На следующем шаге введите имя вашей информационной панели и выберите для нее каталог.

В результате будет пустая панель:

2.3.4. Добавление панелей

Давайте добавим панель графика на нашу панель инструментов:

local grafana = import 'grafonnet/grafana.libsonnet';
grafana.dashboard.new(
    title='My dashboard',
    # allow the user to make changes in Grafana
    editable=true,
    # avoid issues associated with importing multiple versions in Grafana
    schemaVersion=21,
).addPanel(
    grafana.graphPanel.new(
        title='My first graph',
        # demonstration data
        datasource='-- Grafana --'
    ),
    # panel position and size
    gridPos = { h: 8, w: 8, x: 0, y: 0 }
)

Теперь давайте создадим панель инструментов:

jsonnet -J ./vendor dashboard.jsonnet -o dashboard.json

Вы можете перезаписать существующую панель мониторинга или выбрать другое имя при импорте.

Давайте сосредоточимся на gridPos. Размер и положение каждой панели описываются координатами сетки. Ширина приборной панели - 24 единицы. Это означает, что панель _row_ имеет размер 24x1. Когда вы добавляете панель, она по умолчанию имеет размер 12x9.

2.3.5. Настройки визуализации

Панели создаются в результате вызовов функций. Чтобы настроить визуализацию метрики в панелях, нам необходимо изменить параметры этих функций.

local grafana = import 'grafonnet/grafana.libsonnet';
grafana.dashboard.new(
    title='My dashboard',
    editable=true,
    schemaVersion=21,
).addPanel(
    grafana.graphPanel.new(
        title='Pending requests',
        datasource='-- Grafana --',
        # Lowest displayed value
        min=0,
        # Left y-axis legend
        labelY1='pending',
        # Color intensity of the area under the graph
        fill=0,
        # Number of decimal places in values
        decimals=2,
        # Number of decimal places in left y-axis values
        decimalsY1=0,
        # Sort the values in decreasing order
        sort='decreasing',
        # Present the legend as a table
        legend_alignAsTable=true,
        # Display values in the legend
        legend_values=true,
        # Display the average value in the legend
        legend_avg=true,
        # Display the current value in the legend
        legend_current=true,
        # Display the maximum in the legend
        legend_max=true,
        # Sort by current value
        legend_sort='current',
        # Sort in descending order
        legend_sortDesc=true,
    ),
    gridPos = { h: 8, w: 8, x: 0, y: 0 }
)

Все параметры панели описаны в источниках графоннета. В частности, вы можете найти параметры graph в vendor / grafonnet / graph_panel.libsonnet.

2.3.6. Формирование запроса к Прометею

Давайте визуализируем метрику server_pending_requests из нашего тестового приложения.

local grafana = import 'grafonnet/grafana.libsonnet';
 
 grafana.dashboard.new(
     title='My dashboard',
     editable=true,
     schemaVersion=21,
     # Display metrics over the last 30 minutes
     time_from='now-30m'
 ).addPanel(
     grafana.graphPanel.new(
         title='Pending requests',
         # Direct queries to the `Prometheus` data source
         datasource='Prometheus',
         min=0,
         labelY1='pending',
         fill=0,
         decimals=2,
         decimalsY1=0,
         sort='decreasing',
         legend_alignAsTable=true,
         legend_values=true,
         legend_avg=true,
         legend_current=true,
         legend_max=true,
         legend_sort='current',
         legend_sortDesc=true,
     ).addTarget(
         grafana.prometheus.target(
             # Query 'server_pending_requests' from datasource
             expr='server_pending_requests',
             # Label query results as 'alias'
             # (corresponds to specific instances in the application cluster)
             legendFormat='{{alias}}',
         )
     ),
     gridPos = { h: 8, w: 8, x: 0, y: 0 }
 )

Метод _addTarget () _ можно вызывать несколько раз.

2.3.7. Импортировать переменные

Панель управления JSON может включать переменные, определенные при импорте. Таким образом, мы можем настроить источник данных, источник показателей для вашей информационной панели.

local grafana = import 'grafonnet/grafana.libsonnet';
grafana.dashboard.new(
    title='My dashboard',
    editable=true,
    schemaVersion=21,
    time_from='now-30m'
).addInput( # add an import variable
    # Variable name to be used in the dashboard code
    name='DS_PROMETHEUS',
    # Variable name to be displayed on the import screen
    label='Prometheus',
    # This variable defines the data source
    type='datasource',
    # Data will be received from the Prometheus plugin
    pluginId='prometheus',
    pluginName='Prometheus',
    # Variable description to be displayed on the import screen
    description='Prometheus metrics bank'
).addPanel(
    grafana.graphPanel.new(
        title='Pending requests',
        # Use the variable value as a data source
        datasource='${DS_PROMETHEUS}',
        min=0,
        labelY1='pending',
        fill=0,
        decimals=2,
        decimalsY1=0,
        sort='decreasing',
        legend_alignAsTable=true,
        legend_values=true,
        legend_avg=true,
        legend_current=true,
        legend_max=true,
        legend_sort='current',
        legend_sortDesc=true,
    ).addTarget(
        grafana.prometheus.target(
            expr='server_pending_requests',
            legendFormat='{{alias}}',
        )
    ),
    gridPos = { h: 8, w: 8, x: 0, y: 0 }
)

Теперь вы можете выбрать источник данных из раскрывающегося списка, автоматически созданного Grafana.

2.3.8. Динамические переменные

В Grafana есть два типа переменных: __inputs и шаблоны. Хотя __inputs определяются при импорте и «замораживаются» на панели управления, переменные шаблонов можно изменять во время операций.

Работа с __ inputs включает замену текстовых строк $ {VAR_NAME} на пользовательское значение. Однако вы не можете сделать это в запросах к источникам данных - вместо этого вы используете шаблонные переменные.

Один из способов разделения данных, поступающих из разных приложений, - использовать разные Prometheus рабочие места, чтобы собрать их. Вы можете зарегистрировать задание в своем запросе и настроить его при импорте.

 local grafana = import 'grafonnet/grafana.libsonnet';
 
 grafana.dashboard.new(
     title='My dashboard',
     editable=true,
     schemaVersion=21,
     time_from='now-30m'
 ).addInput(
     name='DS_PROMETHEUS',
     label='Prometheus',
     type='datasource',
     pluginId='prometheus',
     pluginName='Prometheus',
     description='Prometheus metrics bank'
 ).addInput( # string constant to be filled in on import
     name='PROMETHEUS_JOB',
     label='Job',
     type='constant',
     pluginId=null,
     pluginName=null,
     description='Prometheus Tarantool metrics job'
 ).addTemplate(
     grafana.template.custom( # dynamic variable
         # Variable name to be used in the dashboard code
         name='job',
         # Initial dynamic variable value is derived from the import variable
         query='${PROMETHEUS_JOB}',
         current='${PROMETHEUS_JOB}',
         # Don't display the variable on the panel screen
         hide='variable',
         # Variable name in the UI
         label='Prometheus job',
     )
 ).addPanel(
     grafana.graphPanel.new(
         title='Pending requests',
         datasource='${DS_PROMETHEUS}',
         min=0,
         labelY1='pending',
         fill=0,
         decimals=2,
         decimalsY1=0,
         sort='decreasing',
         legend_alignAsTable=true,
         legend_values=true,
         legend_avg=true,
         legend_current=true,
         legend_max=true,
         legend_sort='current',
         legend_sortDesc=true,
     ).addTarget(
         grafana.prometheus.target(
             # Use the variable in our request
             expr='server_pending_requests{job=~"$job"}',
             legendFormat='{{alias}}',
         )
     ),
     gridPos = { h: 8, w: 8, x: 0, y: 0 }
 )

В нашем практическом кластере Docker job имеет значение tarantool_app.

Мы применили двухуровневую параметризацию: job регулируется переменной templating, а начальное значение этой переменной определяется при импорте через переменную __inputs. Если вы указали неправильное задание во время импорта, вы можете изменить его в параметрах панели управления.

2.3.9. Использование функций

Давайте добавим еще одну панель, на которой будет отображаться количество запросов в секунду.

local grafana = import 'grafonnet/grafana.libsonnet';
grafana.dashboard.new(
    title='My dashboard',
    editable=true,
    schemaVersion=21,
    time_from='now-30m'
).addInput(
    name='DS_PROMETHEUS',
    label='Prometheus',
    type='datasource',
    pluginId='prometheus',
    pluginName='Prometheus',
    description='Prometheus metrics bank'
).addInput(
    name='PROMETHEUS_JOB',
    label='Job',
    type='constant',
    pluginId=null,
    pluginName=null,
    description='Prometheus Tarantool metrics job'
).addInput( # initial rate_time_range value
    name='PROMETHEUS_RATE_TIME_RANGE',
    label='Rate time range',
    type='constant',
    value='2m',
    description='Time range for computing rps graphs with rate(). Should be two times the scrape interval at least.'
).addTemplate(
    grafana.template.custom(
        name='job',
        query='${PROMETHEUS_JOB}',
        current='${PROMETHEUS_JOB}',
        hide='variable',
        label='Prometheus job',
    )
).addTemplate( # dynamic variable to be used in the query
    grafana.template.custom(
        name='rate_time_range',
        query='${PROMETHEUS_RATE_TIME_RANGE}',
        current='${PROMETHEUS_RATE_TIME_RANGE}',
        hide='variable',
        label='rate() time range',
    )
).addPanel(
    grafana.graphPanel.new(
        title='Pending requests',
        datasource='${DS_PROMETHEUS}',
        min=0,
        labelY1='pending',
        fill=0,
        decimals=2,
        decimalsY1=0,
        sort='decreasing',
        legend_alignAsTable=true,
        legend_values=true,
        legend_avg=true,
        legend_current=true,
        legend_max=true,
        legend_sort='current',
        legend_sortDesc=true,
    ).addTarget(
        grafana.prometheus.target(
            expr='server_pending_requests{job=~"$job"}',
            legendFormat='{{alias}}',
        )
    ),
    gridPos = { h: 8, w: 8, x: 0, y: 0 }
).addPanel( # server load graph
    grafana.graphPanel.new(
        title='Server load',
        datasource='${DS_PROMETHEUS}', 
        min=0,
        labelY1='rps',
        fill=0,
        decimals=2,
        decimalsY1=0,
        sort='decreasing',
        legend_alignAsTable=true,
        legend_avg=true,
        legend_current=true,
        legend_max=true,
        legend_values=true,
        legend_sort='current',
        legend_sortDesc=true,
        legend_rightSide=true,
    ).addTarget(
        grafana.prometheus.target(
            # Compute changes using the data from the rate_time_range period
            expr='rate(server_requests_process_count{job=~"$job"}[$rate_time_range])',
            legendFormat='{{alias}}',
        )
    ),
    # Put the panel on the right of the first one
    gridPos = { h: 8, w: 16, x: 9, y: 0 }
)

Переменная импорта может быть инициализирована в коде.

В итоге у нас есть дашборд с двумя панелями:

Давайте очистим наш код, переместив шаблон графика в функцию:

local grafana = import 'grafonnet/grafana.libsonnet';
local myGraphPanel(
  title=null,
  labelY1=null,
  legend_rightSide=false # default value
) = grafana.graphPanel.new(
    title=title,
    datasource='${DS_PROMETHEUS}',
    min=0,
    labelY1=labelY1,
    fill=0,
    decimals=2,
    decimalsY1=0,
    sort='decreasing',
    legend_alignAsTable=true,
    legend_values=true,
    legend_avg=true,
    legend_current=true,
    legend_max=true,
    legend_sort='current',
    legend_sortDesc=true,
    legend_rightSide=legend_rightSide
);
grafana.dashboard.new(
    title='My dashboard',
    editable=true,
    schemaVersion=21,
    time_from='now-30m'
).addInput(
    name='DS_PROMETHEUS',
    label='Prometheus',
    type='datasource',
    pluginId='prometheus',
    pluginName='Prometheus',
    description='Prometheus metrics bank'
).addInput(
    name='PROMETHEUS_JOB',
    label='Job',
    type='constant',
    pluginId=null,
    pluginName=null,
    description='Prometheus Tarantool metrics job'
).addInput(
    name='PROMETHEUS_RATE_TIME_RANGE',
    label='Rate time range',
    type='constant',
    value='2m',
    description='Time range for computing rps graphs with rate(). Should be two times the scrape interval at least.'
).addTemplate(
    grafana.template.custom(
        name='job',
        query='${PROMETHEUS_JOB}',
        current='${PROMETHEUS_JOB}',
        hide='variable',
        label='Prometheus job',
    )
).addTemplate(
    grafana.template.custom(
        name='rate_time_range',
        query='${PROMETHEUS_RATE_TIME_RANGE}',
        current='${PROMETHEUS_RATE_TIME_RANGE}',
        hide='variable',
        label='rate() time range',
    )
).addPanel(
    myGraphPanel(
        title='Pending requests',
        labelY1='pending'
    ).addTarget(
        grafana.prometheus.target(
            expr='server_pending_requests{job=~"$job"}',
            legendFormat='{{alias}}',
        )
    ),
    gridPos = { h: 8, w: 8, x: 0, y: 0 }
).addPanel(
    myGraphPanel(
        title='Server load',
        labelY1='rps',
        legend_rightSide=true,
    ).addTarget(
        grafana.prometheus.target(
            expr='rate(server_requests_process_count{job=~"$job"}[$rate_time_range])',
            legendFormat='{{alias}}',
        )
    ),
    gridPos = { h: 8, w: 16, x: 9, y: 0 }
)

Возможно, будет удобно переместить шаблон в отдельный файл. Попробуйте сделать это самостоятельно в качестве упражнения. Чтобы узнать о подключении внешних файлов, см. Раздел о Jsonnet выше.

3. Заключительные слова

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

В последней версии моего руководства не хватает нескольких конкретных моментов. У меня достаточно материала на целую вторую статью по этим вопросам. Пожалуйста, оставьте отзыв, если хотите, чтобы он был опубликован!

Подводя итог, ниже несколько полезных ссылок.

Репозиторий grafonnet »Содержит исходный код и документацию Grafonnet, а также несколько примеров использования шаблонов. Кстати, несколько моих пиарщиков все еще ждут рассмотрения, поэтому прошу поддержать их, если они вам интересны.

Исходный код дашборда Tarantool написан на графоннете. Фрагменты кода в моем руководстве и кластер Docker - это результаты моей работы и работы моих коллег над этим проектом. Если у вас есть вопросы по дашборду Tarantool или его расширениям, задавайте их в GitHub Issues или в нашем Telegram-чате. Скачать Tarantool можно на официальном сайте.