Як вбудовувати Grafana-дашборди без головного болю
Ми регулярно отримуємо запити від SaaS-команд: Grafana вже розгорнута, дашборди готові, але вбудувати їх в інтерфейс не виходить. iframe не завантажується через CSP, авторизація вимагає логіну, а браузер блокує сторонні cookies. При цьому кожен клієнт має бачити лише свої дані — multi-tenant обов'язковий. Розповімо, як ми вирішуємо ці завдання та отримуємо робоче embedded-рішення за кілька днів.
Які проблеми вирішує вбудовування Grafana?
Основний біль — безпека. Анонімний iframe надто простий, але для приватних даних не годиться. Другий типовий сценарій — SameSite cookies: браузери все агресивніше блокують міжсайтові сесії. Третій — CSP: якщо в політиці безпеки немає frame-src для вашого Grafana-домену, дашборд не відобразиться. І нарешті, mobile: стандартний інтерфейс Grafana не адаптований під телефони.
Два підходи: anonymous vs embedded
Найпростіший варіант — анонімний iframe. Grafana дозволяє доступ без аутентифікації, ви вставляєте URL дашборду в iframe. Підходить для публічних панелей (моніторинг сайту, uptime). Але якщо дані приватні, потрібна авторизація.
Другий варіант — вбудовування з сервісним акаунтом. Grafana 9.1+ підтримує спеціальні токени, які не вимагають cookies. Ви генеруєте URL на своєму сервері з підписаним токеном і передаєте його в iframe. Користувач навіть не знає, що дивиться дані з Grafana.
| Критерій | Anonymous iframe | Embedded (service account) |
|---|---|---|
| Авторизація | Немає | Є (токен) |
| Безпека | Низька | Висока |
| Multi-tenant | Немає | Так (через vars) |
| Складність налаштування | 1–2 дні | 3–5 днів |
| Підходить для | Публічні дані | Приватні дані, SaaS |
Як налаштувати авторизацію через сервісні акаунти?
Для продакшну використовуйте саме цей спосіб. Конфігурація Grafana:
[security]
allow_embedding = true
[auth.anonymous]
enabled = true
org_role = Viewer
hide_version = true
[cookie]
secure = true
samesite = none
Створіть сервісний акаунт і токен:
curl -X POST http://grafana:3000/api/serviceaccounts \
-H "Content-Type: application/json" \
-u admin:admin \
-d '{"name":"embed-reader","role":"Viewer"}'
curl -X POST http://grafana:3000/api/serviceaccounts/1/tokens \
-H "Content-Type: application/json" \
-u admin:admin \
-d '{"name":"embed-token"}'
Токен зберігайте на сервері і ніколи не віддавайте клієнту.
Генерація signed URL на бекенді
URL для iframe формується на вашому сервері. Приклад на TypeScript:
interface GrafanaEmbedOptions {
dashboardUid: string;
panelId?: number;
from?: string;
to?: string;
vars?: Record<string, string>;
theme?: 'light' | 'dark';
kiosk?: boolean;
}
class GrafanaEmbedService {
constructor(
private readonly baseUrl: string,
private readonly serviceAccountToken: string
) {}
buildEmbedUrl(options: GrafanaEmbedOptions): string {
const { dashboardUid, panelId, from = 'now-24h', to = 'now', vars = {}, theme = 'light', kiosk = true } = options;
const params = new URLSearchParams({ from, to, theme, ...(kiosk ? { kiosk: 'tv' } : {}) });
Object.entries(vars).forEach(([k, v]) => params.append(`var-${k}`, v));
const path = panelId ? `/d-solo/${dashboardUid}?panelId=${panelId}&` : `/d/${dashboardUid}?`;
return `${this.baseUrl}${path}${params.toString()}`;
}
}
Реалізація на фронтенді
React‑компонент для iframe з лоадером та обробкою помилок:
import { useState, useEffect } from 'react';
interface GrafanaPanelProps {
dashboardUid: string;
panelId: number;
vars?: Record<string, string>;
from?: string;
to?: string;
height?: number;
title?: string;
}
export function GrafanaPanel({ dashboardUid, panelId, vars, from = 'now-24h', to = 'now', height = 300, title }: GrafanaPanelProps) {
const [embedUrl, setEmbedUrl] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
fetch('/api/grafana/embed-url', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ dashboardUid, panelId, vars, from, to }),
})
.then(r => r.json())
.then(({ url }) => setEmbedUrl(url))
.catch(() => setError('Не вдалося отримати URL дашборду'));
}, [dashboardUid, panelId, JSON.stringify(vars), from, to]);
if (error) return <div className="text-red-500">{error}</div>;
return (
<div className="relative rounded-lg overflow-hidden border bg-white" style={{ height }}>
{title && <div className="px-4 py-2 border-b text-sm font-medium text-gray-700">{title}</div>}
{loading && <div className="absolute inset-0 flex items-center justify-center bg-gray-50"><div className="w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin" /></div>}
{embedUrl && <iframe src={embedUrl} width="100%" height={title ? height - 40 : height} frameBorder="0" onLoad={() => setLoading(false)} title={title ?? `Grafana panel ${panelId}`} />}
</div>
);
}
API endpoint для генерації URL
Express‑приклад з перевіркою прав:
app.post('/api/grafana/embed-url', requireAuth, async (req, res) => {
const { dashboardUid, panelId, vars, from, to } = req.body;
const hasAccess = await checkDashboardAccess(req.user.id, dashboardUid);
if (!hasAccess) return res.status(403).json({ error: 'Forbidden' });
const url = grafanaService.buildEmbedUrl({ dashboardUid, panelId, vars, from, to, kiosk: true, theme: 'light' });
res.json({ url });
});
Multi-tenant через template variables
Якщо один дашборд використовується для різних клієнтів, передавайте tenant_id як змінну шаблону. Додайте в URL var-tenant_id=123. В джерелі даних Grafana відфільтруйте рядки, наприклад, WHERE tenant_id = '${tenant_id}' AND $__timeFilter(time). Кожен користувач побачить лише свої дані. Для масштабування використовуйте datasource з проксі-сервером, який підставляє tenant_id з токена.
Що робити, якщо CSP блокує iframe?
Додайте домен Grafana до директиви frame-src:
Content-Security-Policy: frame-src https://grafana.yourdomain.com
Також переконайтеся, що SameSide cookie встановлений на none (див. конфігурацію вище). Або, що надійніше, використовуйте сервісні акаунти — вони не покладаються на cookies.
Що входить в роботу з вбудовування?
- Аудит поточної інфраструктури та вимог до безпеки
- Налаштування сервісного акаунта та токенів в Grafana
- Розробка API для генерації signed URL з перевіркою прав
- Інтеграція React-компонента (або Vue/Angular) з обробкою завантаження та помилок
- Налаштування multi-tenant через template variables
- Оптимізація CSP та SameSite cookies
- Тестування на десктопі, планшетах та мобільних пристроях
- Документація для команди
Процес роботи над вбудовуванням
| Етап | Тривалість |
|---|---|
| Аналіз інфраструктури та вимог | 1 день |
| Проектування схеми (авторизація, multi‑tenant) | 1–2 дні |
| Реалізація API та фронтенду | 2–3 дні |
| Тестування в різних браузерах та мобільних | 1–2 дні |
| Документація та передача команді | 1 день |
Ми займаємося вбудовуванням Grafana більше п'яти років, реалізували понад 50 проектів — від пари дашбордів до корпоративних multi‑tenant рішень. В середньому економія порівняно з розробкою власної панелі становить 60–80%. Гарантуємо стабільну роботу та дотримання документації Grafana.
Отримайте консультацію інженера по вашому проекту. Замовте попередню оцінку — ми підберемо оптимальну стратегію та назвемо точні терміни.
Типові помилки при вбудовуванні Grafana
- Токен сервісного акаунта передається в URL клієнта — це небезпечно. Генеруйте URL тільки на сервері.
- Не налаштований
allow_embedding— iframe буде порожнім. - CSP забороняє frame-src — додайте домен Grafana.
- SameSite cookie встановлений як Strict — авторизація зламається в iframe.
- Template variables не екрануються — можлива SQL-ін'єкція, якщо datasource використовує рядкові підстановки.







