Прозрачность рождает доверие — но плохо изолированный дашборд превращается в инцидент по 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. Клиент лишился возможности трогать фильтр и получил число, которое наконец совпало с договором. Урок, который мы переносим в каждый проект: безопасность дашборда живёт не в запросе, а в том месте, где применяется фильтр.
Чек-лист публикации
- Пользователь базы только для чтения, желательно с подключением к реплике.
- Фильтр по организации с учётом поддерева (рекурсивный CTE или список ID).
is_deleted = 0в каждом запросе кglpi_tickets.- Согласованный период: выберите одну дату (создания, решения или закрытия) и используйте её во всех карточках.
- Фильтр зафиксирован на сервере (подписанный embed), а не только в URL.
- Проверка ссылки внешним пользователем перед отправкой клиенту.
Нужны клиентские дашборды с изоляцией по организации, защищённой от утечек? NexTool внедряет и сопровождает среды GLPI со встроенной BI (Metabase и Grafana) и управлением данными в соответствии с LGPD. Узнайте об услуге поддержки и сопровождения или посмотрите сравнение Metabase и Grafana и ключевые KPI для Service Desk.
Проверено командой NexTool Solutions.