Общая панель для клиента в GLPI

Как создавать дашборды обслуживания и открывать к ним доступ клиентам с помощью Metabase или Grafana поверх GLPI: фильтр по организации (включая дочерние), корректный расчёт SLA в базе и доступ, защищённый от подмены.

В этой статьеРазделов: 8

Прозрачность рождает доверие — но плохо изолированный дашборд превращается в инцидент по LGPD. Если делиться с клиентом показателями обслуживания, становится меньше требований по e-mail, а вы демонстрируете профессионализм. Техническая сложность не в том, чтобы нарисовать график, а в том, чтобы каждый клиент видел только свои заявки. Разберём, как сделать это с GLPI + Metabase или Grafana, зафиксировав фильтр по организации в правильном месте.

Прежде чем открывать доступ: три решения, которые определяют всё

В сопровождении мультиклиентских сред GLPI три выбора решают, станет ли дашборд конкурентным преимуществом или проблемой безопасности:

  • Где фиксировать фильтр по организации: в ссылке, в запросе или в подписанном embed. Только последний устойчив к подмене URL.
  • Какую дату использовать для периода: создания (date), решения (solvedate) или закрытия (closedate). Смешивание всех трёх искажает все показатели.
  • Откуда читать данные: из основной базы или из реплики только для чтения. Metabase и Grafana запускают тяжёлые запросы и конкурируют с GLPI в продакшене.

Шаг 1 — пользователь базы данных только для чтения

Никогда не подключайте BI-инструмент под пользователем GLPI. Создайте отдельного пользователя с SELECT и ничем больше. В MariaDB:

-- Read-only user for BI, restricted to the internal network
CREATE USER 'bi_ro'@'10.20.%' IDENTIFIED BY '...strong_password...';
GRANT SELECT ON glpi.* TO 'bi_ro'@'10.20.%';

-- Per-query time limit keeps a dashboard from stalling the database (MariaDB)
SET GLOBAL max_statement_time = 15;   -- seconds

FLUSH PRIVILEGES;

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

Шаг 2 — изоляция по организации без утечки дочерних организаций

Каждый клиент в GLPI — это организация, и первое желание — фильтровать по entities_id = X. Здесь кроется самая частая ловушка: если у клиента есть филиалы или подразделения в виде дочерних организаций, такой фильтр захватывает только корневую организацию, и цифры получаются меньше реальных. Нужно включить всё поддерево. В MariaDB 10.2.2+ это решается рекурсивным CTE:

-- All of the client's entities (root + descendants)
WITH RECURSIVE client_tree AS (
    SELECT id FROM glpi_entities WHERE id = 4        -- client's root entity
    UNION ALL
    SELECT e.id
    FROM glpi_entities e
    JOIN client_tree c ON e.entities_id = c.id
)
SELECT
    COUNT(*)                       AS total,
    SUM(t.status IN (1,2,3,4))     AS open_tickets,
    SUM(t.status IN (5,6))         AS closed_tickets
FROM glpi_tickets t
WHERE t.entities_id IN (SELECT id FROM client_tree)
  AND t.is_deleted = 0;

Обратите внимание на is_deleted = 0: GLPI использует логическое удаление, и без этого фильтра в подсчёт попадают заявки из корзины. Это вторая по частоте ошибка, которую мы исправляем в дашбордах, унаследованных от другой команды.

Шаг 3 — SLA без завышения процента

Доля выполнения SLA кажется тривиальной: решили до срока — засчитали. Обманчивая деталь — поле time_to_resolve (срок SLA, записанный в заявке): оно равно NULL, когда к заявке не привязан SLA. Если сравнивать solvedate <= time_to_resolve, не обрабатывая NULL, такие заявки выпадают из знаменателя, и процент искусственно растёт. Задайте выборку явно:

WITH RECURSIVE client_tree AS (
    SELECT id FROM glpi_entities WHERE id = 4
    UNION ALL
    SELECT e.id FROM glpi_entities e
    JOIN client_tree c ON e.entities_id = c.id
)
SELECT
    COUNT(*)                                                          AS with_sla,
    SUM(t.solvedate <= t.time_to_resolve)                          AS on_time,
    ROUND(100 * SUM(t.solvedate <= t.time_to_resolve) / COUNT(*), 1) AS sla_pct
FROM glpi_tickets t
WHERE t.entities_id IN (SELECT id FROM client_tree)
  AND t.is_deleted = 0
  AND t.time_to_resolve IS NOT NULL      -- only tickets with an SLA
  AND t.solvedate IS NOT NULL            -- already solved
  AND t.date >= '2026-06-01'
  AND t.date <  '2026-07-01';

Какие показатели имеют смысл для клиента

Не всё, что отслеживает внутренняя команда, интересно клиенту — или должно ему показываться. Лучше работает компактный набор:

  • Операционные: открытые заявки по статусу и приоритету, открытые и закрытые за период, среднее время ответа и решения.
  • SLA: процент в пределах срока и заявки под угрозой (близкие к истечению time_to_resolve).
  • Удовлетворённость: средняя оценка опроса (glpi_ticketsatisfactions) и месячная динамика.
  • Договор: израсходованные и законтрактованные часы и остаток, если есть пакет часов.

Где фиксировать фильтр: матрица решений

Самый деликатный момент — не дать клиенту изменить параметр в URL и увидеть чужие данные. Каждый способ применяет фильтр в своём месте:

СпособГде применяется фильтрРиск утечкиТрудозатратыКогда использовать
Metabase — публичная ссылкаРедактируемый параметр в URLВысокийНизкиеТолько для внутренней демонстрации, никогда с данными клиента
Metabase — подписанный embed (JWT)Параметр зафиксирован на сервере (токен)НизкийСредниеРекомендуемый стандарт для клиента
Grafana — анонимный доступОткрывает всю организацию GrafanaВысокийНизкиеИзбегать
Grafana — Public Dashboards (9.1+)Один дашборд на ссылку, без редактируемой переменнойСреднийНизкиеОдин клиент, без чувствительных дочерних организаций
Grafana — организация на клиентаИзоляция по org + проксиНизкийВысокиеМного клиентов, сильная изоляция

Ловушка, которая обошлась дороже всего (в сопровождении)

В среде с десятками организаций дашборд одного клиента показывал примерно половину заявок, которые фиксировал ручной отчёт. Причина была не в запросе подсчёта, а в публичной ссылке Metabase. Фильтр по организации был параметром дашборда, и любой, у кого была ссылка, мог изменить значение в URL и просматривать другие организации. Мы заменили её на подписанный embed (JWT), с entities_id, вшитым в токен, и параметром, помеченным как locked. Клиент лишился возможности трогать фильтр и получил число, которое наконец совпало с договором. Урок, который мы переносим в каждый проект: безопасность дашборда живёт не в запросе, а в том месте, где применяется фильтр.

Чек-лист публикации

  1. Пользователь базы только для чтения, желательно с подключением к реплике.
  2. Фильтр по организации с учётом поддерева (рекурсивный CTE или список ID).
  3. is_deleted = 0 в каждом запросе к glpi_tickets.
  4. Согласованный период: выберите одну дату (создания, решения или закрытия) и используйте её во всех карточках.
  5. Фильтр зафиксирован на сервере (подписанный embed), а не только в URL.
  6. Проверка ссылки внешним пользователем перед отправкой клиенту.

Нужны клиентские дашборды с изоляцией по организации, защищённой от утечек? NexTool внедряет и сопровождает среды GLPI со встроенной BI (Metabase и Grafana) и управлением данными в соответствии с LGPD. Узнайте об услуге поддержки и сопровождения или посмотрите сравнение Metabase и Grafana и ключевые KPI для Service Desk.


Проверено командой NexTool Solutions.

Часто задаваемые вопросы

Фильтр только по entities_id = X захватывает лишь корневую организацию. Чтобы включить филиалы и подразделения, постройте поддерево рекурсивным CTE по glpi_entities (MariaDB 10.2.2+) и используйте entities_id IN (SELECT id FROM arvore_cliente). В качестве альтернативы сформируйте список ID в приложении и передайте его параметром.

Потому что time_to_resolve равно NULL, когда к заявке не привязан SLA. При сравнении solvedate <= time_to_resolve без отсева таких случаев заявки без SLA выпадают из знаменателя и завышают процент. Ограничьте выборку условием time_to_resolve IS NOT NULL.

Для чувствительных данных — нет. В публичной ссылке фильтр по организации находится в URL и может быть изменён, что открывает данные других клиентов. Используйте подписанный embed (JWT) с параметром, помеченным как locked и применяемым на сервере. Публичная ссылка годится только для внутренней демонстрации.

Предпочтительнее реплика только для чтения. Если реплики нет, создайте как минимум пользователя с GRANT SELECT и задайте max_statement_time, чтобы тяжёлый запрос дашборда не удерживал транзакции GLPI в продакшене.

Зависит от показателя. Для входящего объёма используйте date (создание). Для производительности и SLA — solvedate (решение) или closedate (закрытие). Частая ошибка — смешивать: подсчёт решённых заявок по дате создания занижает результат месяца.

Сравните solvedate с time_to_resolve в заявке: SUM(solvedate <= time_to_resolve) от общего числа заявок с SLA. Отфильтруйте is_deleted = 0, time_to_resolve IS NOT NULL и solvedate IS NOT NULL, чтобы знаменатель был корректным.

Нужна помощь?