惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
N
News and Events Feed by Topic
D
DataBreaches.Net
MongoDB | Blog
MongoDB | Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Engineering at Meta
Engineering at Meta
T
Tailwind CSS Blog
博客园_首页
Microsoft Azure Blog
Microsoft Azure Blog
Y
Y Combinator Blog
博客园 - Franky
Hugging Face - Blog
Hugging Face - Blog
月光博客
月光博客
A
About on SuperTechFans
I
InfoQ
S
Securelist
Last Week in AI
Last Week in AI
S
Schneier on Security
C
CXSECURITY Database RSS Feed - CXSecurity.com
Hacker News: Ask HN
Hacker News: Ask HN
Schneier on Security
Schneier on Security
Know Your Adversary
Know Your Adversary
腾讯CDC
大猫的无限游戏
大猫的无限游戏
S
Security @ Cisco Blogs
博客园 - 三生石上(FineUI控件)
Simon Willison's Weblog
Simon Willison's Weblog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
美团技术团队
aimingoo的专栏
aimingoo的专栏
G
Google Developers Blog
罗磊的独立博客
Vercel News
Vercel News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Cloudflare Blog
S
Secure Thoughts
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Latest news
Latest news
Recent Announcements
Recent Announcements
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
L
LINUX DO - 热门话题
Security Latest
Security Latest
TaoSecurity Blog
TaoSecurity Blog
Cyberwarzone
Cyberwarzone
有赞技术团队
有赞技术团队

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Дашборд для SSRS и Power BI Report Server, которого нет ни у кого: собираем сами и забираем готовым
Максим Юдин · 2026-06-16 · via Все публикации подряд на Хабре

Средний

16 мин

257

Скажу честно: к этой статье меня подтолкнула причина донельзя прозаическая. Сижу недавно на брифинге, и прилетает жалоба - сервер отчётов работает из рук вон плохо. Сервер при этом не мой, в орбиту обслуживания он не входил, да и сам я последнее время вожусь с публикой помоложе и помоднее, Superset и FineBI. А тут пришлось тряхнуть стариной и вспомнить, как оно всё устроено у SSRS. Вспомнил. Заодно собрал то, чего мне самому когда-то не хватало, - и решил, что пора всё это выложить в одну статью.

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

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

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

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

Коротко

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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» или «источник данных недоступен», готовый текст для передачи дальше. Этот дочерний дашборд тоже входит в сборку.

Чем сервер занят, когда никого нет

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

Это фоновое обновление моделей .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 при отображении прибавит к ней те же три часа, и вы получите время из будущего. Я на этом споткнулся - в таблице подписок «последний запуск» показывал время, которого ещё не наступило.

На чём всё стоит

Архитектура простая и разделённая по природе данных.

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

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

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

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

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

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

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

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

Маленькая ремарка из практики, которая сэкономит вам полчаса. Если на одной ноде сбор есть, а на другой нет - первым делом проверьте, не читает ли служба конфиг из другого файла. У меня на одном сервере экспортёр запускался с 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

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

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 - единицы миллисекунд. Десятки и сотни - диск перегружен и тормозит рендеринг. На виртуалках с общим хранилищем чуть выше нормы из-за слоя виртуализации, это ожидаемо.

Руководство по установке

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

Шаг 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 не собирает метрики в разрезе процессов, а они нужны - чтобы видеть отдельно сам сервис отчётов и движок 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. Все панели подписок, ошибок и тяжёлых отчётов читают отсюда. Если у вас именованный инстанс или база называется иначе - поправьте имя в запросах (везде ReportServer.dbo.*).

  • 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).

Забрать всё одним пакетом - в оригинале статьи на моем сайте в блоге. В дашбордах нет ни реальных хостов, ни строк подключения - datasource подставите свои при импорте.

Что в итоге получилось

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

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

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

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

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

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

А чем вы смотрите за своими отчётными серверами - или они у вас живут как чёрный ящик, пока не прилетит «рассылка не пришла»? Любопытно, кто как решает.