В блог

Как создавать дашборды Grafana AI-агентами, не тратя миллионы токенов

Как создавать дашборды Grafana AI-агентами, не тратя миллионы токенов

Попросить AI-агента создать дашборд Grafana с десятками панелей несложно. Но если агент работает через веб-интерфейс, задача может растянуться на множество действий. Ему приходится открывать редактор, настраивать запросы, выбирать визуализации, сохранять изменения и повторять всё это для каждой панели

На каждом этапе агент обрабатывает состояние интерфейса, получает скриншоты и решает, что делать дальше. Это расходует токены, а при большом количестве панелей увеличивает вероятность ошибок

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

Разберём, как настроить такой процесс

Перенос дашбордов в JSON

Первым делом нужно сделать файлы основным способом управления дашбордами. Grafana поддерживает provisioning – механизм, который загружает конфигурацию из файлов и отслеживает её изменения

Использовать его имеет смысл и без AI-агентов. Когда дашборды хранятся в репозитории, их можно версионировать, проверять через Git и развёртывать через CI/CD. По сути, это применение принципов Infrastructure as Code к настройке мониторинга

Если дашборды уже существуют, экспортируйте их из Grafana через Export as code. Выберите модель Classic и сохраните JSON-файлы в репозитории. В зависимости от версии Grafana расположение команды экспорта может отличаться

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

Эту подготовительную работу тоже можно поручить агенту, если у него есть доступ к Grafana и репозиторию

Для примера используем следующую структуру проекта

.
├── docker-compose.yml
├── AGENTS.md
├── scripts/
│   └── dashboard-context.py
└── grafana/
    ├── provisioning/
    │   ├── alerting/
    │   ├── plugins/
    │   ├── datasources/
    │   └── dashboards/
    │       └── dashboards.yaml
    └── dashboards/
        └── requests.json

Здесь grafana/dashboards/ содержит определения самих дашбордов, а grafana/provisioning/ – настройки их загрузки

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

Настройка файлового provisioning

Создайте файл grafana/provisioning/dashboards/dashboards.yaml

apiVersion: 1
providers:
  - name: dashboards
    type: file
    allowUiUpdates: false
    disableDeletion: true
    updateIntervalSeconds: 30
    options:
      path: /var/lib/grafana/dashboards

Здесь path указывает каталог с JSON-файлами внутри контейнера, а updateIntervalSeconds определяет интервал проверки изменений

При значении 30 Grafana будет периодически проверять каталог. Это удобно при использовании Docker bind mounts, поскольку события изменения файлов не всегда корректно передаются внутрь контейнера

Параметр allowUiUpdates: false запрещает сохранять изменения provisioned-дашбордов через интерфейс. Это помогает избежать ситуации, когда в базе Grafana оказывается одна версия дашборда, а в репозитории – другая

disableDeletion: true предотвращает автоматическое удаление дашборда из Grafana при исчезновении соответствующего файла. Если же вы хотите, чтобы удаление файла из репозитория приводило к удалению дашборда, установите false

Теперь подключите каталоги к существующему сервису Grafana в docker-compose.yml

services:
  grafana:
    volumes:
      - ./grafana/provisioning:/etc/grafana/provisioning:ro
      - ./grafana/dashboards:/var/lib/grafana/dashboards:ro

Здесь показаны только дополнительные монтирования. Остальные настройки сервиса, включая существующие тома и переменные окружения, нужно сохранить

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

С этого момента агент может создавать и изменять панели непосредственно в репозитории. А CI/CD сможет доставлять обновлённые JSON-файлы на сервер, где работает Grafana

Выборочное чтение JSON

После перехода на provisioning исчезает необходимость редактировать каждую панель через веб-интерфейс. Но появляется другая проблема

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

Если попросить агента изменить один SQL-запрос, а он прочитает весь файл, значительная часть контекста будет занята сведениями, которые к задаче не относятся

Особенно заметно это при последовательной работе с несколькими панелями. Агент может повторно читать одни и те же большие файлы, хотя ему нужны только отдельные поля

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

Скрипт для чтения панелей

Добавим файл scripts/dashboard-context.py⁠. Для его работы достаточно стандартной библиотеки Python

import argparse
import json
import sys
from pathlib import Path


def panels(items):
    if not isinstance(items, list):
        raise ValueError("panels must be an array")
    for panel in items:
        if not isinstance(panel, dict):
            raise ValueError("panel must be an object")
        yield panel
        yield from panels(panel.get("panels", []))


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("file", type=Path)
    parser.add_argument(
        "command",
        choices=["list", "panel", "queries", "variables"]
    )
    parser.add_argument("--id", type=int)
    args = parser.parse_args()

    try:
        data = json.loads(
            args.file.read_text(encoding="utf-8")
        )
        if not isinstance(data, dict):
            raise ValueError("dashboard must be an object")

        dashboard = data.get(
            "dashboard", data.get("spec", data)
        )
        if not isinstance(dashboard, dict):
            raise ValueError("dashboard must be an object")
        if not isinstance(dashboard.get("panels"), list):
            raise ValueError(
                "expected Classic JSON with a panels array; "
                "V2 is not supported"
            )

        if args.command == "variables":
            result = dashboard.get(
                "templating", {}
            ).get("list", [])
        else:
            items = list(
                panels(dashboard.get("panels", []))
            )

            if args.command == "list":
                result = [
                    {
                        "id": p.get("id"),
                        "title": p.get("title"),
                        "type": p.get("type")
                    }
                    for p in items
                ]
            else:
                if args.id is None:
                    parser.error(
                        "panel and queries require --id"
                    )

                matches = [
                    p for p in items
                    if p.get("id") == args.id
                ]
                if len(matches) != 1:
                    raise ValueError(
                        f"expected one panel, found {len(matches)}"
                    )

                panel = matches[0]

                result = (
                    panel
                    if args.command == "panel"
                    else {
                        "datasource": panel.get("datasource"),
                        "targets": panel.get("targets", []),
                        **{
                            key: panel[key]
                            for key in (
                                "interval",
                                "maxDataPoints",
                                "timeFrom",
                                "timeShift",
                                "hideTimeOverride",
                                "libraryPanel"
                            )
                            if key in panel
                        }
                    }
                )

        print(json.dumps(
            result,
            ensure_ascii=False,
            separators=(",", ":")
        ))

    except (
        OSError,
        ValueError,
        TypeError,
        AttributeError,
        RecursionError
    ) as error:
        sys.exit(f"Cannot read dashboard: {error}")


if __name__ == "__main__":
    main()

Скрипт поддерживает четыре команды

  • list – список панелей с их ID, названиями и типами
  • queries – запросы и связанные настройки конкретной панели
  • panel – полная конфигурация одной панели
  • variables – переменные дашборда

Например, чтобы найти нужную панель, агент может выполнить

python3 scripts/dashboard-context.py grafana/dashboards/requests.json list

Вместо всего JSON он получит компактный список

[
  {"id":1,"title":"HTTP requests","type":"timeseries"},
  {"id":12,"title":"Errors by service","type":"timeseries"},
  {"id":15,"title":"Response time","type":"stat"}
]

Если нужно изменить запрос панели с ID 12⁠, достаточно выполнить

python3 scripts/dashboard-context.py grafana/dashboards/requests.json queries --id 12

Чтобы получить остальные настройки этой панели

python3 scripts/dashboard-context.py grafana/dashboards/requests.json panel --id 12

А если запрос зависит от переменных дашборда

python3 scripts/dashboard-context.py grafana/dashboards/requests.json variables

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

Скрипт читает исходный JSON целиком, но обрабатывает его локально и отправляет в контекст агента лишь результат выбранной команды. Именно за счёт уменьшения объёма вывода и достигается экономия токенов

При этом инструмент предназначен для чтения конфигурации, а не для её изменения. Агент по-прежнему редактирует JSON-файл, но теперь ему не требуется предварительно загружать в контекст все его настройки

В примере используется Classic JSON с массивом panels⁠. Для Dashboard JSON V2 нужна отдельная реализация, поскольку структура определения дашборда отличается. Кроме того, скрипт не раскрывает зависимости library panels и связанных настроек

Правила работы агента в AGENTS.md

Сам по себе скрипт не гарантирует, что агент начнёт его использовать. Если не объяснить порядок работы, он может продолжить открывать JSON-файлы целиком и выполнять лишние действия

Поэтому добавим инструкцию в AGENTS.md⁠, который агент читает при работе с репозиторием

# Дашборды Grafana

- Дашборды: grafana/dashboards/*.json, загрузка через provisioning.
- Редактируй конфигурацию через файлы и команды.
- Не читай все дашборды целиком. Используй скрипт:
  python3 scripts/dashboard-context.py <файл> list
  python3 scripts/dashboard-context.py <файл> queries --id <ID>
  python3 scripts/dashboard-context.py <файл> panel --id <ID>
  python3 scripts/dashboard-context.py <файл> variables
- Сам находи нужные схемы таблиц и настройки переменных.
- Исключай логи и выгрузки из поиска, если они не нужны для задачи.
- Меняй только нужные поля, сохраняй UID дашборда, ID панелей и остальные настройки.
- Связанные панели и library panels проверяй отдельно: скрипт не раскрывает зависимости.
- Если есть генератор, меняй его исходники и запускай сборку.
- Сам проверяй синтаксис JSON, уникальность ID и diff.
- Проверяй изменённые запросы через источник данных тестовой Grafana с используемыми переменными и периодом.
- Проверяй загрузку в тестовую Grafana и данные изменённой панели. При изменении оформления проверяй внешний вид.
- Исправляй ошибки и повторяй проверки. Если нет доступа, сообщи о блокировке.
- В ответе кратко укажи изменения и результаты проверок.

Здесь важны не только команды для чтения JSON, но и ограничения на редактирование

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

Отдельно стоит требовать проверки результата. Корректный JSON ещё не означает, что панель работает. SQL-запрос может обращаться к несуществующему полю, переменная – подставлять неподходящее значение, а выбранная визуализация – не соответствовать структуре данных

Проверять это агент тоже может самостоятельно, если у него есть доступ к тестовой Grafana и источнику данных

Как агент изменяет панель

Предположим, нужно изменить панель с количеством HTTP-запросов. Сейчас она показывает общее число обращений, а требуется разбить их по кодам ответа

Агент сначала получает список панелей через list и находит нужный ID. Затем командой queries читает существующий запрос и определяет, какие данные используются

Если в запросе есть переменные, агент дополнительно получает их определения через variables⁠. При необходимости изучает схему таблицы в ClickHouse, PostgreSQL или другом подключённом источнике данных

После этого он изменяет соответствующий участок JSON-файла, проверяет синтаксис и просматривает diff. При наличии тестового окружения проверяет запрос и загрузку обновлённой панели в Grafana

Для базовой проверки корректности JSON можно использовать стандартную команду

python3 -m json.tool grafana/dashboards/requests.json > /dev/null

Если файл содержит синтаксическую ошибку, команда завершится с ошибкой. Но для полноценной проверки всё равно потребуется Grafana, поскольку json.tool не проверяет корректность структуры дашборда и запросов к источникам данных

После успешных проверок изменения можно отправлять в репозиторий и развёртывать обычным способом через CI/CD

Таким образом, агент работает преимущественно с нужными фрагментами конфигурации, а не с веб-интерфейсом Grafana. Большая часть действий выполняется через локальные команды и редактирование файлов

Что даёт работа через файлы

Основная экономия достигается не за счёт самого JSON и не за счёт provisioning. Эти механизмы лишь позволяют организовать работу с дашбордами как с кодом

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

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

Насколько сильно сократится расход токенов, зависит от сложности дашборда, размера JSON, используемой модели и количества проверок. Фиксированного коэффициента экономии здесь нет

Но главное преимущество остаётся независимо от конкретных цифр. Дашборды становятся частью привычного процесса разработки – с Git, code review, автоматическими проверками и развёртыванием через CI/CD. А AI-агент получает возможность работать с ними без постоянного обращения к веб-интерфейсу

Соберём мониторинг под ваш проект

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

Обсудить мониторинг Мониторинг и оповещения