Встраивание Grafana-дашбордов в веб-приложение
Grafana умеет встраиваться в сторонние приложения через iframe. Это дешевле, чем строить собственный дашборд с нуля, и разумно когда у вас уже есть Grafana с настроенными datasource и дашбордами — нужно только показать их внутри своего интерфейса, возможно с авторизацией через SSO.
Два режима встраивания
Anonymous iframe — самый простой. Grafana настраивается на разрешение анонимного доступа к конкретным дашбордам. Подходит для публичных дашбордов (мониторинг сервисов, публичная статистика).
Grafana Embedded (Grafana 9.1+) — встраивание с токеном сервисного аккаунта. Iframe получает JWT-токен, Grafana проверяет его без пользовательского логина. Правильный выбор для приложений с аутентификацией.
Конфигурация Grafana
# grafana.ini
[security]
allow_embedding = true
[auth.anonymous]
enabled = true
org_role = Viewer
hide_version = true
# Для production — ограничить CORS
[security]
cookie_secure = true
cookie_samesite = none
Grafana Service Account Token (рекомендуемый способ)
# Создаём сервисный аккаунт через API
curl -X POST http://grafana:3000/api/serviceaccounts \
-H "Content-Type: application/json" \
-u admin:admin \
-d '{"name":"embed-reader","role":"Viewer","isDisabled":false}'
# Создаём токен для сервисного аккаунта
curl -X POST http://grafana:3000/api/serviceaccounts/1/tokens \
-H "Content-Type: application/json" \
-u admin:admin \
-d '{"name":"embed-token","secondsToLive":0}'
Генерация signed URL на сервере
Чтобы конечный пользователь не видел токен сервисного аккаунта, URL генерируется на вашем сервере:
interface GrafanaEmbedOptions {
dashboardUid: string;
panelId?: number;
from?: string; // e.g. 'now-7d'
to?: string; // e.g. 'now'
vars?: Record<string, string>; // template variables
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(([key, val]) => {
params.append(`var-${key}`, val);
});
const path = panelId
? `/d-solo/${dashboardUid}?panelId=${panelId}&`
: `/d/${dashboardUid}?`;
return `${this.baseUrl}${path}${params.toString()}`;
}
// Для Grafana 10+ с auth token в URL (не рекомендуется для production)
buildAuthenticatedUrl(options: GrafanaEmbedOptions, userEmail: string): string {
const baseEmbed = this.buildEmbedUrl(options);
// В production используйте OAuth/SSO вместо передачи токена в URL
return baseEmbed;
}
}
Компонент встраивания
import { useState, useEffect, useRef } 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 iframeRef = useRef<HTMLIFrameElement>(null);
useEffect(() => {
// URL генерируется на нашем сервере, не в браузере
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));
}, [dashboardUid, panelId, JSON.stringify(vars), from, to]);
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
ref={iframeRef}
src={embedUrl}
width="100%"
height={title ? height - 40 : height}
frameBorder="0"
onLoad={() => setLoading(false)}
title={title ?? `Grafana panel ${panelId}`}
/>
)}
</div>
);
}
API endpoint для генерации URL
// Express / Fastify
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 });
});
Template Variables для multi-tenant
Если один дашборд используется для разных клиентов, Grafana template variables позволяют фильтровать данные:
// Каждый пользователь видит только свои данные
const embedUrl = grafanaService.buildEmbedUrl({
dashboardUid: 'analytics-overview',
vars: {
tenant_id: req.user.tenantId,
region: req.user.region,
},
from: 'now-30d',
to: 'now',
});
В Grafana datasource query эта переменная используется как фильтр:
SELECT * FROM metrics WHERE tenant_id = '${tenant_id}' AND $__timeFilter(time)
Проблемы и решения
CSP блокирует iframe — добавьте домен Grafana в frame-src:
Content-Security-Policy: frame-src https://grafana.yourdomain.com
SameSite cookie — при встраивании в iframe Grafana-сессионные cookies могут блокироваться браузером. Решение: сервисные аккаунты с токеном вместо сессионных cookies.
Мобильные устройства — кнопки Grafana UI мелкие на телефоне. Если нужен полноценный мобильный опыт, встраивание не подходит — нужны собственные компоненты.
Сроки
Встраивание готовых дашбордов с генерацией URL на сервере и проверкой прав доступа — 2–4 дня. Multi-tenant конфигурация с template variables — ещё 1–2 дня.







