К этой статье меня подтолкнула причина донельзя прозаическая. Сижу недавно на брифинге, и прилетает жалоба: сервер отчётов работает из рук вон плохо. Сервер при этом не мой, в орбиту обслуживания он не входил, а тут пришлось вспомнить, как оно всё устроено у SQL Server Reporting Services (SSRS), вспомнить молодость, так сказать. Вспомнил. Заодно собрал то, чего мне самому когда-то не хватало, — и решил, что пора всё это выложить в одну статью.

Сервер отчётов — сервис незаметный, пока кто-нибудь из бизнеса не напишет: «а почему мне со вчерашнего дня не приходит утренняя рассылка». Сидишь, открываешь портал, видишь, что подписка вроде есть, вроде активна, а письма не уходят. Ну и как оно всегда было? Лезешь в логи, а логов нормальных нет, есть только таблица где-то внутри базы, в которую редко кто, кроме DBA, и заглядывал.

Знакомо? Если вы держите SSRS или его старшего брата Power BI Report Server, наверняка знакомо.

Вопросы, на которые SSRS обязан отвечать сам, звучат просто.

  • Какие рассылки упали этой ночью и по какой причине?

  • Какие отчёты открывают чаще всего, а какие не открывали полгода? Это обычная статистика использования отчётов, которой в портале нет.

  • Где сервер реально упирается в ресурсы?

  • Кто владелец подписки, которая ломается третью неделю подряд?

Штатными средствами ни один из этих ответов не достаётся: портал показывает список объектов, а журнал выполнения лежит таблицей в служебной базе, без графиков поверх. Ниже — как я собрал дашборд в Grafana поверх базы ReportServer и какие грабли попались по дороге.

Я полез искать готовый дашборд. По уму так, чтобы взять, накатить, и видеть сразу: какие рассылки падают, какие отчёты открываются по минуте, что вообще творится на сервере ночью, когда пользователей нет. Знаете, что я нашёл? Десяток одинаковых сборок «SQL Server overview» с CPU, памятью и числом сессий. Безликие счётчики, которые к мониторингу SSRS имеют отношение постольку-поскольку. Ни одного, который бы лез внутрь ReportServer и показывал то что нужно, то есть отчёты, их нагрузку и их доставку.

Пришлось собирать самому. Заняло это, прямо скажем, прилично времени. Но получилось то, чем не стыдно пользоваться каждый день — и вот об этом расскажу подробно, с запросами, граблями и парой неочевидных вещей, на которые я убил несколько часов.

Коротко

  • Готового вменяемого дашборда под SSRS/PBIRS в сети нет, только обобщённые «обзоры SQL Server», которые не показывают ни сбоев подписок, ни тяжёлых отчётов, ни фоновой активности pbix.

  • Всё ценное лежит внутри базы ReportServer: таблицы ExecutionLog, Subscriptions, SubscriptionHistory, Catalog. Microsoft их официально не документирует, так что приходится разбираться по факту.

  • Power BI отчёты (.pbix) ведут себя в логе совсем не так, как обычные пагинированные (.rdl) — и если этого не знать, метрики врут.

  • Мониторинг SSRS строится на двух источниках: прямые SQL-запросы к ReportServer (бизнес-метрики) и windows_exporter через VictoriaMetrics (инфраструктура нод).

  • По дороге всплыла классика — расхождение времени между Grafana и MSSQL на величину часового пояса. Лечится тонким слоем view поверх базы.

  • PromQL-запросы к VictoriaMetrics один в один работают и в Prometheus — стек взаимозаменяем.

Всю сборку — оба дашборда в JSON, скрипт создания view и рабочий конфиг экспортёра — выложил на GitHub, ссылка в конце. Накатывается за полчаса, дальше только свои источники подставить.

Почему вообще понадобился отдельный дашборд

SQL Server Reporting Services живёт с нами очень давно, и за эти годы у него так и не появилось внятной системы наблюдения. Парадокс: система зрелая, стоит в каждом втором энтерпрайзе, генерит критичную для бизнеса отчётность, а смотреть на её здоровье нечем.

С приходом Power BI Report Server стало только хуже. Теперь на одном сервере соседствуют два мира: классические пагинированные отчёты .rdl и интерактивные Power BI отчёты .pbix. Ведут они себя по-разному, нагружают сервер по-разному, и пишут в журнал тоже по-разному. А мониторинг Reporting Services, если он вообще есть, обычно покрывает в лучшем случае «жив ли сервис».

Мне же нужно было видеть конкретные вещи. Не абстрактную загрузку CPU, а ответы на рабочие вопросы:

  • Какие отчёты упали при открытии и с какой ошибкой — чтобы отдать трейс разработчику отчёта, а не разводить руками.

  • Какие подписки и рассылки не доставлены — и кому они принадлежат, чтобы было с кого спросить.

  • Какие отчёты тяжёлые — где сервер реально потеет, выгребая данные из источника.

  • Чем сервер занят ночью, когда живых пользователей нет, а нагрузка почему-то есть.

Ни один готовый дашборд из тех, что я видел, на эти вопросы не отвечает, потому что ответы лежат внутри самой базы ReportServer, а не в системных счётчиках Windows. Копать пришлось именно туда, в служебную базу Reporting Services.

Что внутри базы ReportServer

Сразу важная оговорка: Microsoft не документирует схему ReportServer. Официально. Это внутренняя база Reporting Services, и её структуру могут менять между версиями без предупреждения. Никакого справочника полей, как для системных представлений SQL Server, по ней не существует и не появится.

Поэтому всё, что ниже, это результат раскопок по факту, на живой базе, со сверкой через sys.columns и неофициальные источники вроде mssqltips. Проверяйте на своей версии, поля могут отличаться.

Ещё одна вещь, которую стоит знать до того, как накатите дашборд. Журнал выполнения хранится не вечно: по умолчанию Reporting Services держит записи 60 дней, дальше ночное задание их вычищает. Управляется это параметром ExecutionLogDaysKept в таблице ConfigurationInfo. Ноль означает «хранить бесконечно», и вот так делать не надо: на нагруженном сервере ExecutionLogStorage пухнет быстро, особенно если в хозяйстве много .pbix с их десятками строк на одно открытие. Если нужна история за год, правильнее не раздувать срок хранения, а перекладывать данные в отдельную таблицу своим расписанием.

Ключевые таблицы, которые будем смотреть:

ExecutionLogStorage — журнал выполнения. Каждый запуск отчёта, каждое открытие, каждая отправка подписки оставляет здесь строку. Поверх неё есть представления ExecutionLog, ExecutionLog2, ExecutionLog3 — они декодируют числовые коды в человекочитаемый вид. Я работал через ExecutionLog3. Главные поля:

  • ItemPath — путь к отчёту.

  • UserName — кто запускал.

  • RequestType — тип запроса: Interactive (пользователь открыл), Subscription (рассылка), Refresh Cache (фоновое обновление данных).

  • Format — формат вывода: RPL (просмотр на экране), PDF, EXCELOPENXML, MHTML и так далее.

  • ItemAction — действие: Render, ConceptualSchema, DataRefresh, SaveToCatalog и прочие.

  • TimeStart, TimeEnd — начало и конец.

  • TimeDataRetrieval, TimeProcessing, TimeRendering — три фазы выполнения в миллисекундах. Вот это золото для поиска тяжёлых отчётов.

  • Status — результат. rsSuccess или код ошибки.

  • ByteCount, RowCount — объём результата и число строк.

Subscriptions — сами подписки. Кто владелец, какое расписание, какое описание, когда последний запуск.

SubscriptionHistory — история отработки подписок. Вот тут важный момент: ошибки доставки рассылок видны именно здесь, а не в ExecutionLog. Об этом дальше отдельно.

Catalog — все объекты сервера: отчёты, папки, источники данных. Тип объекта в поле Type: 2 — пагинированный отчёт (.rdl), 13 — Power BI отчёт (.pbix), 1 — папка, и так далее.

Users — пользователи. Сюда джойнятся все эти OwnerID, CreatedByID, ModifiedByID, чтобы вместо GUID показать живое имя.

Самое коварное: pbix — это не rdl

Вот здесь было немного приседаний, и про это нигде толком не написано.

Когда вы открываете обычный пагинированный отчёт .rdl, сервер его рендерит и записывает в лог реальные фазы: столько-то на выборку данных, столько-то на обработку, столько-то на рендеринг, так что всё измеримо и разложено по полочкам.

С Power BI отчётом всё иначе. Рендеринг .pbix происходит в браузере у пользователя, а не на сервере. Поэтому фазы времени в логе по нему — нули. Сервер только отдаёт модель данных и обслуживает запросы визуалов, а рисует всё клиент.

Что это означает на практике. Одно открытие .pbix-отчёта порождает в логе не одну строку, а целую пачку. Один заход пользователя — это:

  • одна строка ConceptualSchema (собственно факт открытия отчёта),

  • и следом десятки строк QueryData — по запросу на каждый визуал на странице.

Если вы наивно посчитаете «вызовы отчёта» через COUNT(*), у вас один любопытный аналитик, потыкавший в дашборд, превратится в сотню «вызовов». Я это увидел на реальных данных: 70 строк в логе за пять минут схлопывались в 8 настоящих взаимодействий, на большем срезе — 354 сырых строки на 19 реальных открытий.

Как считать правильно? Открытие .pbix-отчёта — это одна строка с ItemAction = 'ConceptualSchema'. Не QueryData, не ASModelStream — только ConceptualSchema. Вот надёжный маркер:

SELECT COUNT(*) AS pbix_opens
FROM ReportServer.dbo.ExecutionLog3
WHERE ItemAction = 'ConceptualSchema'
  AND TimeStart >= @from AND TimeStart < @to;

Одна запись — одно открытие. Проверено на живых данных: совпадает с реальным числом заходов, без шума визуалов.

Раз уж зашла речь про фазы. Для панели «тяжёлые отчёты», которая считает среднее время по TimeDataRetrieval + TimeProcessing + TimeRendering, нужно .pbix исключать вообще. Иначе их нули занижают средние, и реально медленные пагинированные отчёты тонут в этой массе. Фильтр простой: Format NOT IN ('PBIX', 'DataModel') AND Format IS NOT NULL.

Подписки: ошибки доставки лежат в отдельной таблице

Ещё одна неочевидная вещь. Казалось бы, упала рассылка, значит смотри в ExecutionLog. Так вот, там их нет и не будет.

ExecutionLog со строкой RequestType = 'Subscription' фиксирует, что отчёт под рассылку сгенерировался, а вот доставлен ли он, ушло ли письмо, записался ли файл на сетевую шару — это уже в SubscriptionHistory, и события это разные. Отчёт может прекрасно отрендериться (в ExecutionLog будет rsSuccess), а письмо не уйдёт, потому что почтовый сервер прилёг. В ExecutionLog тишина, а в SubscriptionHistory — ошибка.

Поэтому «упавшие подписки» надо считать из SubscriptionHistory, где Status > 0. Вот так выглядит сводка по проблемным рассылкам, с владельцем, описанием и числом ошибок за период:

SELECT
    s.SubscriptionID,
    c.Path AS ReportPath,
    u.UserName AS Owner,
    s.Description,
    s.LastStatus,
    s.LastRunTime,
    she.ErrorsInPeriod
FROM ReportServer.dbo.Subscriptions s
JOIN ReportServer.dbo.[Catalog] c ON c.ItemID = s.Report_OID
LEFT JOIN ReportServer.dbo.Users u ON u.UserID = s.OwnerID
INNER JOIN (
    SELECT
        sh.SubscriptionID,
        SUM(CASE WHEN sh.Status > 0 THEN 1 ELSE 0 END) AS ErrorsInPeriod
    FROM ReportServer.dbo.SubscriptionHistory sh
    GROUP BY sh.SubscriptionID
) she ON she.SubscriptionID = s.SubscriptionID
WHERE she.ErrorsInPeriod > 0
ORDER BY s.LastRunTime DESC;

Маленькая деталь, на которой легко обжечься: джойн к Users обязательно LEFT. Если у подписки удалили владельца (уволился человек, вычистили учётку из AD), а сама подписка осталась, при INNER JOIN она просто пропадёт из выборки, а это как раз самые интересные подписки, «осиротевшие». Прятать их как раз не стоит, иначе половина проблем останется невидимой.

И ещё. В таблице Subscriptions живут не только рассылки. Запланированное обновление данных .pbix (тот самый scheduled refresh) реализовано через тот же механизм подписок и сидит здесь же, с EventType = 'DataModelRefresh'. Если вы считаете «настоящие» рассылки, эти записи надо отфильтровать: EventType <> 'DataModelRefresh'. Без этого refresh-задачи раздувают статистику подписок и картина перестаёт сходиться с реальностью.

Отдельно стоит сказать про трейс для разработчиков. Сводная таблица показывает, что подписка падала и сколько раз — но разработчику отчёта этого мало, ему нужен текст ошибки. Поэтому я сделал клик по строке проваливающимся во второй дашборд, «История подписки». Технически это data link на ячейке: при клике Grafana передаёт SubscriptionID и путь отчёта через URL в переменные дочернего дашборда, и тот показывает всю историю доставки именно этой подписки с расшифрованными сообщениями об ошибках.

Сообщения, кстати, лежат в поле Details записи истории — это JSON, внутри массив Errors с полем Message. Достаются они через OPENJSON с JSON_VALUE, кириллица декодируется, несколько сообщений склеиваются в одну ячейку. Получается, что инженер кликнул по упавшей рассылке — и сразу видит «не удалось подключиться к SMTP» или «источник данных недоступен», готовый текст для передачи дальше. Дочерний дашборд для этого тоже лежит в сборке, отдельно настраивать ничего не нужно.

Чем PBIRS занят ночью, когда никого нет

Любимая моя панель. Открываешь утром графики, а ночью, в три часа, когда ни одного живого пользователя, случается всплеск нагрузки. Откуда?

Это фоновое обновление моделей .pbix. Сервер по расписанию подтягивает свежие данные в импортированные модели, чтобы утром пользователь видел актуальные цифры. В логе это RequestType = 'Refresh Cache' под системной учёткой, и цикл из трёх стадий: DataRefresh (тянет данные из источника), ASModelStream (загоняет модель в движок) и SaveToCatalog (пишет обновлённую модель обратно в базу).

Вот на SaveToCatalog есть ByteCount — размер записанной модели. По нему можно построить график «объём записи моделей при обновлении» и наглядно видеть, когда и насколько сервер сам себя нагружает:

SELECT
    $__timeGroup(TimeStartUtc, '10m', 0) AS time,
    SUM(CAST(ByteCount AS BIGINT)) AS [Объём записи моделей]
FROM ReportServer.dbo.ExecutionLog3_utc
WHERE RequestType = 'Refresh Cache'
  AND Status = 'rsSuccess'
  AND ItemAction = 'SaveToCatalog'
  AND $__timeFilter(TimeStartUtc)
GROUP BY $__timeGroup(TimeStartUtc, '10m', 0)
ORDER BY 1;

Важно понимать, что именно показывают эти байты. Это размер уже сжатой модели, которую сервер записал обратно в каталог — в саму базу ReportServer. То есть вес финального артефакта, а не объём сырых данных, вычитанных из источника (он обычно в разы больше, VertiPaq хорошо жмёт). Практический вывод простой: тяжёлые .pbix реально раздувают вам базу и бэкапы. У меня попадались модели под полтора гигабайта на одно обновление. Полезно знать в лицо такие отчёты, они первыми упираются в лимиты.

Кстати, заметьте имя view в запросе — ExecutionLog3_utc. Не ExecutionLog3. История, стоящая за этой буквой, заслуживает отдельного разбора.

История про три часа, которые сломали половину дашборда

Когда основные панели заработали, я наткнулся на странность. Беру пик активности на графике, вижу аномальные 172 события в одну точку. Иду в SSMS проверить — а там по этому времени ноль. Пусто. Запрос тот же, данные те же, цифры разные.

Это классика связки Grafana плюс SQL, и грабли эти ловят многих.

Дело в том, что Grafana ожидает время в колонке всегда в UTC. А Reporting Services пишет TimeStart в локальном времени сервера. У меня это московское, плюс три часа от UTC. Когда Grafana формирует фильтр по времени, она шлёт в запрос границы в UTC, а сравнивает их с колонкой, которая в локальном времени. Сдвиг ровно на три часа. «Пик в 21:10» на графике физически выбирал данные за 18:10 — то есть ночной пик рассылок, где этих событий и правда под две сотни.

Самое противное: настройкой таймзоны дашборда это не лечится. Потому что источников два. VictoriaMetrics отдаёт время в UTC, как и положено. А MSSQL — в локальном. Любая глобальная настройка зоны починит один источник и сломает второй. Поставишь UTC — поедут графики windows_exporter. Поставишь местное — поедут SQL-панели.

Правильное решение — привести оба источника к UTC. VictoriaMetrics уже в UTC, значит надо подтянуть MSSQL. Тут есть красивый ход: не трогать ни системные таблицы (это запрещено и небезопасно), ни каждый запрос по отдельности, а сделать тонкий слой view, который добавляет к данным колонку с пересчитанным временем.

CREATE OR ALTER VIEW dbo.ExecutionLog3_utc AS
SELECT *,
    DATEADD(HOUR, DATEDIFF(HOUR, GETDATE(), GETUTCDATE()), TimeStart) AS TimeStartUtc
FROM dbo.ExecutionLog3;

DATEDIFF(HOUR, GETDATE(), GETUTCDATE()) вычисляет смещение сервера от UTC прямо на лету, без хардкода тройки. Сервер сам себя спрашивает «на сколько я отличаюсь от UTC» и сдвигает. Перевели на зимнее время, переехали в другой регион — формула продолжит работать.

Таких view получилось три: для ExecutionLog3, для SubscriptionHistory (там колонка StartTime) и для Subscriptions (там LastRunTime). Дальше все панели переключаются на эти view и на UTC-колонки — и в фильтрах времени, и в выводимых датах. После этого оба источника живут в одной зоне, таймзона дашборда ставится в браузерную, и всё сходится: и графики из VictoriaMetrics, и таблицы из SQL показывают одно и то же время.

Нюанс, который стоит запомнить при модификации дашборда: в SQL-панелях UTC-колонку надо использовать везде — и в фильтрах, и в выводе. Если оставить в SELECT обычную локальную колонку, Grafana при отображении прибавит к ней те же три часа, и вы получите время из будущего. Я на этом споткнулся: в таблице подписок «последний запуск» показывал время, которого ещё не наступило. (не пугайтесь, в дашборде по ссылке на GitHub уже всё настроено)

Архитектура мониторинга: SQL-метрики и windows_exporter

Бизнес-метрики — прямыми SQL-запросами к базе ReportServer. Всё, что про отчёты, подписки, ошибки, тяжесть — это туда. Grafana с MSSQL-датасорсом, запросы вы видели выше.

Инфраструктура нод — через windows_exporter, который отдаёт метрики в VictoriaMetrics, а Grafana их оттуда читает. Это CPU, память, диски серверов, где крутится сам отчётный сервис.

Отдельно отмечу: PromQL-запросы, которые я приведу для VictoriaMetrics, работают один в один в Prometheus. Это совместимый язык запросов, так что если у вас Prometheus вместо VictoriaMetrics, просто берите и используйте, менять ничего не надо.

windows_exporter: включаем сбор по процессам

По умолчанию windows_exporter не собирает метрики в разрезе процессов. А нам это нужно, чтобы видеть отдельно потребление самого сервиса PBIRS и встроенного движка Analysis Services, который обслуживает .pbix-модели.

Включается коллектор process в конфиге экспортёра:

collectors:
  enabled: "[defaults],process"
collector:
  process:
    include: .+
    exclude: ''

После правки — перезапуск службы, и через минуту в хранилище появляются метрики windows_process_* по всем процессам ноды.

Маленькая ремарка из практики, которая сэкономит вам полчаса. Если у вас двухнодовый SSRS и на одной ноде сбор есть, а на другой нет — первым делом проверьте, не читает ли служба конфиг из другого файла. У меня на одном сервере экспортёр запускался с config.yml, а правил я по привычке config.yaml. Расширения разные, файл живой — другой. Правки уходили в пустоту, метрики не появлялись. Минут сорок я искал проблему не там. Проверяется одной строкой:

(Get-CimInstance win32_service | Where-Object Name -eq 'windows_exporter').PathName

В выводе будет видно, какой файл реально читает служба.

Метрики из VictoriaMetrics

Память встроенного Analysis Services. Движок msmdsrv держит в памяти модели данных .pbix. Именно он раздувается при обновлении моделей и при активной работе пользователей с Power BI отчётами. По сути — сколько оперативки съели Power BI модели:

windows_process_working_set_bytes{process="msmdsrv", instance=~"$rs_instance"}

Загрузка процессора этим же движком. Тут есть тонкость с интерпретацией. Метрика нормируется на одно ядро, поэтому значения легко переваливают за сто процентов — триста означает работу на трёх ядрах одновременно. Это нормально: обновление моделей идёт в несколько потоков.

sum by (instance) (rate(windows_process_cpu_time_total{process="msmdsrv", instance=~"$rs_instance"}[$__rate_interval])) * 100

Задержка дисков нод. Для Power BI Report Server диск часто оказывается узким местом — временные файлы при рендеринге тяжёлых отчётов, кэш. Средняя задержка операции считается как отношение накопленного времени к числу операций, отдельно по чтению и записи или общая:

rate(windows_logical_disk_read_write_latency_seconds_total{instance=~"$rs_instance", volume=~"[A-Z]:"}[$__rate_interval])
/
clamp_min(
  rate(windows_logical_disk_reads_total{instance=~"$rs_instance", volume=~"[A-Z]:"}[$__rate_interval])
  +
  rate(windows_logical_disk_writes_total{instance=~"$rs_instance", volume=~"[A-Z]:"}[$__rate_interval]),
  1
)

Тут пара рабочих хитростей. Фильтр volume=~"[A-Z]:" оставляет только тома с буквами и отсекает служебные разделы вроде HarddiskVolume1, которые только шумят. А clamp_min(..., 1) страхует от деления на ноль, когда операций в интервале не было.

Здоровые значения для SSD — единицы миллисекунд. Десятки и сотни — диск перегружен и тормозит рендеринг. На виртуалках с общим хранилищем чуть выше нормы из-за слоя виртуализации, это ожидаемо.

Установка дашборда в Grafana: пять шагов

Импорт несложный, но есть несколько шагов, которые надо выполнить именно в этом порядке, иначе панели приедут пустыми.

Шаг 1. Создать три view в базе ReportServer

Панели читают время не из системных представлений напрямую, а из трёх view с пересчитанной в UTC колонкой — ExecutionLog3_utc, SubscriptionHistory_utc, Subscriptions_utc. Без них SQL-панели выдадут ошибку «invalid object name», поэтому шаг идёт первым.

Готовый скрипт — файл create_view_report_server.sql из сборки. Выполняется один раз в базе ReportServer:

USE [ReportServer]
GO

CREATE OR ALTER VIEW dbo.ExecutionLog3_utc AS
SELECT *,
    DATEADD(HOUR, DATEDIFF(HOUR, GETDATE(), GETUTCDATE()), TimeStart) AS TimeStartUtc
FROM dbo.ExecutionLog3;
GO

CREATE OR ALTER VIEW dbo.SubscriptionHistory_utc AS
SELECT *,
    DATEADD(HOUR, DATEDIFF(HOUR, GETDATE(), GETUTCDATE()), StartTime) AS StartTimeUtc
FROM dbo.SubscriptionHistory;
GO

CREATE OR ALTER VIEW dbo.Subscriptions_utc AS
SELECT *,
    DATEADD(HOUR, DATEDIFF(HOUR, GETDATE(), GETUTCDATE()), LastRunTime) AS LastRunTimeUtc
FROM dbo.Subscriptions;
GO

Эти view ничего не меняют в системных таблицах — они только добавляют сверху колонку с временем, пересчитанным в UTC. DATEDIFF(HOUR, GETDATE(), GETUTCDATE()) вычисляет смещение сервера от UTC на лету, без хардкода. Системные объекты при этом не затрагиваются, так что последующие обновления Reporting Services ничего не сломают.

Шаг 2. Включить коллектор process в windows_exporter

По умолчанию windows_exporter не собирает метрики в разрезе процессов, а они нужны — чтобы видеть отдельно сам сервис SSRS и движок Analysis Services (msmdsrv), который обслуживает .pbix-модели.

В конфиге экспортёра (файл config.yml из сборки, целиком) коллектор process должен быть в списке включённых, плюс блок с фильтром процессов:

collectors:
  enabled: "[defaults],license,process,scheduled_task,textfile,time,vmware"
collector:
  process:
    include: .+
    exclude: ''

После правки — перезапуск службы: Restart-Service windows_exporter. Через минуту в хранилище появятся метрики windows_process_*.

Две вещи, на которых легко потерять время. Первое: убедитесь, что служба читает именно тот файл, который вы правите — проверяется через (Get-CimInstance win32_service | Where-Object Name -eq 'windows_exporter').PathName. Расширения .yml и .yaml в одной папке — разные файлы, и правка не в тот уходит впустую. Второе: если служба не стартует после правки — чаще всего порт 9182 занят зависшим процессом экспортёра, ищется через netstat -ano | findstr ":9182".

Шаг 3. Импортировать оба дашборда

В Grafana: Dashboards → New → Import → Upload JSON. Сначала основной дашборд, затем дашборд истории подписки. При импорте Grafana запросит источники данных:

  • MSSQL — подключение к базе ReportServer. Все панели подписок, ошибок и тяжёлых отчётов читают отсюда.

  • VictoriaMetrics или Prometheus — метрики windows_exporter. Отсюда процессор, память, диски нод. Запросы совместимы с обоими хранилищами.

Дашборд истории подписки просит только MSSQL. Привяжите оба дашборда к одному и тому же MSSQL-датасорсу — иначе провал по клику из таблицы упавших подписок работать не будет.

Шаг 4. Настроить переменную с нодами

Это самый важный шаг, и дальше возможны два сценария в зависимости от того, как у вас устроен парк серверов.

Несколько нод (scale-out PBIRS). Дашборд тянет список нод динамически — переменная rs_instance спрашивает у метрик, какие хосты есть:

label_values(windows_os_hostname{hostname=~"$host_filter"},instance)

$host_filter — вторая переменная, куда вписывается регулярка под имена ваших серверов. По умолчанию стоит пример srv-rs-0[12].*. Поменяйте на свой паттерн — и список инстансов подтянется сам. Новый сервер с windows_exporter попадёт в список автоматически, без правки дашборда.

Частая ошибка: если паттерн в host_filter не совпадает с реальными именами хостов, список будет пустым и инфраструктурные панели окажутся без данных. Симптом «узлы не подтягиваются» — почти всегда это. Проверьте в Explore, что windows_os_hostname{hostname=~"ваш-паттерн"} вообще что-то возвращает. Учтите: в части версий экспортёра имя хоста лежит в windows_cs_hostname, а не в windows_os_hostname — если первая пустая, пробуйте вторую.

Один инстанс SSRS. Динамический список не нужен. Откройте переменную host_filter и впишите туда точное имя своего сервера (или маску под него) — единственная нода подтянется. Лишнюю плашку сверху уберите: в настройках переменной host_filter поставьте Show on dashboardNothing. Вписали имя один раз — и забыли.

Шаг 5. Проверить вспомогательные переменные

Ещё две переменные работают из коробки, трогать обычно не нужно:

  • report — выпадающий список всех отчётов (тянется из Catalog), фильтрует панели по конкретному отчёту. __ALL__ показывает всё.

  • fail_class — скрытый переключатель классов сбоев (.rdl / открытия .pbix / refresh) для фильтрации.

После этих пяти шагов дашборд оживёт полностью: SQL-панели увидят view, инфраструктурные — метрики нод, провал в историю подписки заработает.

Где забрать сборку

Весь комплект для развёртывания лежит в репозитории, файлами, которые можно сразу скачать:

  • Основной дашборд — 21 панель: счётчики сбоев, таблицы подписок и отчётов с трейсом, производительность, фоновая нагрузка, здоровье нод.

  • Дашборд истории подписки — открывается по клику из таблицы упавших подписок, показывает расшифрованные ошибки доставки.

  • Скрипт создания view — три UTC-view, выполнить в базе ReportServer (Шаг 1).

  • Конфиг windows_exporter — рабочий конфиг с включённым коллектором process (Шаг 2).

Забрать всё одним пакетом: https://github.com/maxpiter/ssrs-pbirs-grafana-dashboard

В дашбордах нет ни реальных хостов, ни строк подключения — датасорсы подставите свои при импорте.

Что показывает готовый дашборд

Дашборд, который отвечает на рабочие вопросы вместо показа безликих графиков.

Сверху — счётчики сбоев: сколько подписок упало, сколько отчётов не открылось, сколько подписок осиротело без владельца. Дальше — таблицы с трейсом: какой отчёт, какая ошибка, кто запускал, кто последним менял. Этот трейс удобно передать разработчику отчёта — вместо «у меня не работает» он получает конкретный код ошибки и время. Заодно получается аудит обращений: видно, кто открывал отчёт, когда и с каким результатом.

Тут напрашивается следующий шаг, до которого у меня пока не дошли руки. Раз в таблице уже есть ModifiedByID — кто последним правил отчёт, — можно навесить на эти панели алертинг Grafana и при сбое автоматически дёргать того, кто за отчёт отвечает. Если отчётами занимается команда аналитиков, это закрывает разрыв «сломалось у пользователя — автор узнал через неделю»: автор получает уведомление сразу, в момент падения, с готовым трейсом. Пока это в планах, но архитектурно всё для этого на дашборде уже лежит.

Ниже — производительность: время по фазам для пагинированных отчётов, топ самых тяжёлых, разбивка нагрузки. Отдельно — фоновая активность .pbix: открытия по ConceptualSchema, обновления моделей, объём записи в каталог. В самом низу — здоровье нод: процессор и память сервиса, задержки дисков, паспорт серверов с характеристиками.

Времени ушло прилично. Причём большую его часть съели не запросы, а вот эти неочевидные грабли: .pbix врёт про фазы, ошибки доставки лежат в другой таблице, время разъезжается на часовой пояс. Зато теперь, когда приходит «а почему мне не пришла рассылка», ответ находится за пятнадцать секунд. Раньше на это уходило полдня раскопок в логах.

Если соберётесь повторять — главное, держите в голове, что .pbix живёт по своим правилам, не по правилам .rdl. Это сэкономит вам тот самый день, который потратил я.

Пишите вопросы, что вспомню — отвечу. Надеюсь, будет в помощь.