Як вбудовувати 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 використовує рядкові підстановки.







