Не просто дашборд: как DORA-метрики помогли нам сократить инциденты на 80%

Не просто дашборд: как DORA-метрики помогли нам сократить инциденты на 80% - 1

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

В Островке из этой задачи в итоге вырос отдельный сервис: он собирает события о релизах, связывает их с изменениями в GitLab, сопоставляет с инцидентами и отдает данные в Grafana. На MVP мы покрыли 90% проектов, а после регулярного разбора метрик и автоматизации узких мест количество критичных сбоев по последним данным снизилось на 80%.

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


Привет, Хабр! Я Алёна Тямейкина, Senior Python-разработчик в Островке. Сегодня расскажу, как мы внедряли DORA-метрики на практике: откуда брали данные, как считали показатели для разных сценариев релиза и где потом все это показывали. И, конечно, о том, какие изменения это запустило в релизных процессах и как повлияло на стабильность прода.

В основе DORA лежит очень яркий и на первый взгляд даже немного провокационный тезис: чем выше скорость разработки кода, тем выше качество. Не в смысле «нужно просто бежать быстрее», а в том, что зрелые инженерные процессы позволяют и быстрее доставлять изменения, и реже ломать прод.

В этой статье я говорю о четырех классических DORA-метриках:

  1. Deployment Frequency. Как часто команда успешно выпускает код в рабочую среду (делает релиз)?

  2. Lead Time for Changes. Сколько времени проходит от создания кода до его успешного запуска в продовой среде?

  3. Change Failure Rate. Насколько часто вам приходится откатывать изменения из продовой среды?

  4. Mean Time to Recovery. Какое количество времени проходит между потерей работоспособности сервиса до её восстановления?

Позднее в DORA появилась и пятая метрика — Reliability. В Островке мы тоже её отслеживаем: за надёжность и стабильность эксплуатации у нас отвечает отдельная команда, которая следит за показателями доступности, скорости ответа сервисов и другими связанными метриками. Но поскольку в моем кейсе Reliability не входила в контур внедрения, дальше я сосредоточусь на четырёх основных метриках.

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

Но чтобы эти показатели действительно что-то значили, сначала нужно было собрать для них надежную основу: данные о релизах, merge request’ах и инцидентах. 

Речь шла о 30 командах и более чем 150 проектах, которые суммарно могли выпускать свыше 50 релизов в день. При этом данные были разнесены по разным системам, а процессы у команд заметно отличались. Где-то использовались релизные ветки, где-то — схема, близкая к git flow, а где-то были свои локальные особенности. Поэтому перед расчётом метрик сначала пришлось выстроить единый контур данных.

По сути, передо мной стояли три вопроса:

  • откуда все это собирать;

  • как считать;

  • куда сохранять и как визуализировать.

Отвечать на них я, разумеется, взялась с помощью Python.

Как устроен сервис для сбора DORA-метрик

На первом этапе нужно было собрать отдельный сервис, который возьмёт на себя всю техническую часть вокруг DORA-метрик. Его задача была довольно простой по формулировке: получать события о деплоях, дополнять их данными из GitLab и корпоративного таск-трекера, а затем сохранять результат в виде, пригодном для расчётов и визуализации.

Здесь важны были три источника данных:

  • события о самих деплоях;

  • данные из GitLab о том, какие изменения вошли в конкретный релиз;

  • данные из корпоративного трекера об инцидентах, связанных с этими изменениями.

    Не просто дашборд: как DORA-метрики помогли нам сократить инциденты на 80% - 2

С нуля этот контур собирать не пришлось. В качестве базы я использовала внутренний сервис автоматизаций, который у нас уже существовал (подробности в статье). Он хорошо подходил под задачу: находился рядом с остальными сервисами, уже выполнял вспомогательные функции и, что особенно важно, его структура данных частично совпадала с тем, что требовалось для DORA. Это позволило расширить существующую платформу под новый сценарий.

В итоге решение получилось на базе Django-сервиса с таким стеком: Django 5, django-ninja, ARQ, Postgres, Redis, GitLab Client и отдельный клиент для корпоративного трекера.

Этот стек закрывал все наши требования:

Требование

Решение

Хранение данных — на долгий срок и в структурированном виде

Лучше всего подходила реляционная модель, поэтому основным хранилищем стал Postgres

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

Стандартный Django admin — понятный и достаточно удобный для внутренних задач

Нормальная работа с БД и предсказуемая серверная логика

Django закрывал это из коробки и хорошо подходил под сервис, в котором много моделей, связей и служебной бизнес-логики

API-слой для приёма событий и внутренних операций

Django-ninja дал удобную схему описания эндпойнтов, валидацию через pydantic и автогенерируемую документацию

Инструмент для фоновых и периодических задач — прежде всего для синхронизации инцидентов

ARQ — в этом случае он оказался проще и легче, чем более тяжёлые очереди вроде Celery

Redis использовали для очередей, Postgres — для постоянного хранения. Для интеграций добавили два клиента: один для GitLab, второй для корпоративного трекера. Клиент для трекера пришлось написать отдельно, потому что готовое решение не покрывало нужный набор действий и не подходило под нашу модель работы с API.

Не просто дашборд: как DORA-метрики помогли нам сократить инциденты на 80% - 3

Строим модель данных

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

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

Не просто дашборд: как DORA-метрики помогли нам сократить инциденты на 80% - 4

Смысл модели был в том, чтобы опираться не на абстрактные события, а на понятные сущности, с которыми потом удобно работать и в коде, и в аналитике.

  • Team — это команда, для которой потом строятся агрегированные графики и рассчитываются командные показатели.

  • Project — отдельный сервис или репозиторий, привязанный к команде. У одной команды таких проектов может быть несколько.

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

  • Feature — изменения, которые вошли в этот релиз. Через них мы дальше считали, что именно доехало до прода и сколько времени занял путь изменений.

  • Incident — критический сбой, связанный с релизом. Эти данные понадобились для метрик, связанных с неудачными выкладками и временем восстановления.

Такое разбиение оказалось полезно не только для хранения, но и для отображения в Grafana: можно было без лишних костылей собирать как проектные, так и командные срезы, а затем строить агрегации по времени, сервисам и командам.

Подробнее про расчёт метрик

1. Deployment Frequency: как мы фиксировали релизы

Здесь все началось с практической проблемы: в компании не было одного-единственного варианта деплоя. Где-то использовались Docker и Ansible, где-то Kubernetes и Argo, где-то собственные CI/CD-сценарии, а в отдельных случаях — другие локальные настройки и процессы.

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

Не просто дашборд: как DORA-метрики помогли нам сократить инциденты на 80% - 5

Поэтому мы сделали один API-эндпойнт, который стал общей точкой входа для всех деплойных сценариев. По сути, он играл роль универсального хука: принимал событие о релизе, валидировал его, приводил к общему формату и возвращал код ответа. 

Такой формат был выбран не только из-за удобства, но и из-за технических ограничений. Некоторые процессы, в частности Ansible в нашей внутренней реализации, ориентировались только на HTTP-статус ответа. Если вернуть что-то сложнее, чем код 200/400/500, можно было повлиять на сам деплой. Поэтому внешний контракт эндпойнта сделали максимально простым, а всю диагностику оставили в логах.

Вот сокращенный фрагмент обработчика, который показывает саму логику приема релиза: фильтрацию production-событий, определение проекта, сбор изменений и запись релиза в едином формате:

@router.get(
    '/changelog',
    summary='Get changelog info from Tower',
    description='Get changelog info from Tower',
    response=BaseResponse[Release],
    tags=['DORA endpoints']
)
async def get_changelog_from_tower(
    request: HttpRequest,
    params: ChangelogParamsIn = Query(...)
) -> HttpResponse:
    # Проверка production-среды и версий релиза
    if params.source != 'production' or (
        params.previous_version and int(params.previous_version) > int(params.version)
    ):
        return HttpResponse(status=200)

    # Выбираем проект и команду
    try:
        project, team = await get_project_and_team(name=params.project)
    except Project.DoesNotExist:
        return HttpResponse(status=404)

    # Подгружаем предыдущую версию
    if params.previous_version is None:
        params.previous_version = project.last_release_build

    features = await get_release_features(
        project=project,
        release_time=params.release_datetime or dt.datetime.now(dt.timezone.utc),
        prod_version=params.version,
        dev_version=params.previous_version,
    )

    # Обработка features, расчет метрик и создание release_data
    release_data = build_release_data(
        team_name=team.name,
        project=project,
        features=features,
        release_time=params.release_datetime or dt.datetime.now(dt.timezone.utc),
        prod_version=params.version
    )

    # Сохраняем релиз - если данные есть
    if features:
        await Releases.objects.create_entire_release(
            release_data=release_data,
            team=team,
            project=project,
        )
    return HttpResponse(status=200)

После приёма события нужно было понять, можно ли вообще считать его релизом для Deployment Frequency. В базу попадали только production-выкладки. Дополнительно мы отсекали ситуации, которые не должны считаться новым релизом: например, откаты или повторные выкладки той же версии. Отдельно проверяли успешность релиза по служебным эндпойнтам, которые создаются для сервисов внутри компании: эндпойнт должен был вернуть 200, а версия приложения — совпасть с выкатываемой. Если сервис отвечал 500 или версия расходилась, такой релиз успешным не считался.

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

Ещё одна важная часть для этой метрики — корректное время релиза. По умолчанию мы использовали момент получения события сервисом, то есть utc now. Но в эндпойнт можно было передать и явный timestamp выкладки. Изначально это понадобилось для загрузки исторических данных, а затем осталось как резервный механизм на случай, если релиз прошёл, а событие по какой-то причине не дошло до сервиса обработки метрик.

Это было важно не только для самой Deployment Frequency. Точное время релиза потом становилось опорной точкой для следующей метрики — Lead Time for Changes, где уже считается, сколько времени изменение добиралось до прода. Поэтому ошибка на этом этапе искажала не только частоту релизов, но и следующие расчёты.

В итоге Deployment Frequency у нас считалась не по косвенным признакам и не по данным из какого-то одного инструмента, а по зафиксированным production-релизам, приведенным к единому формату.

2. Lead Time for Changes: от чего именно считать путь изменения до прода

Если с Deployment Frequency основной проблемой было зафиксировать сам факт релиза, то с Lead Time for Changes все оказалось сложнее. Здесь нужно не просто поймать событие, а определить, от какой точки вообще считать путь изменения до прода.

Опять же, даже внутри одной компании процесс разработки и релиза устроен по-разному. Если команда выкатывает изменения сразу после merge, путь до прода выглядит одним образом. Если изменения копятся до отдельного релиза или проходят через релизные ветки, картина уже другая. В такой ситуации одна универсальная трактовка Lead Time быстро начинает давать слишком грубую картину.

Поэтому для метрики мы использовали два подхода.

1. От создания Merge Request до релиза

Мы обозначили его как CMR (Created time of Merge Request). Эта метрика показывает, сколько времени проходит от момента, когда изменение уже оформлено в виде merge request, до его попадания в прод. Такой подход дает более широкий взгляд на путь изменения: сюда попадает и само ожидание ревью, и доработки, и время до релиза.

2. От merge Merge Request до релиза

Это MMR (Merged time of Merge Request). Здесь merge становится точкой, после которой код считается готовым и остается понять, сколько он еще ждет реального релиза. Эта метрика особенно полезна, когда нужно увидеть задержки уже не в разработке, а именно в релизном процессе.

Не просто дашборд: как DORA-метрики помогли нам сократить инциденты на 80% - 6

Мы рассматривали вариант считать Lead Time от статусов задач в корпоративном трекере, но быстро отказались от этой идеи. Изменение статуса задачи не гарантирует, что разработчик в этот момент действительно начал писать код или, наоборот, закончил с ним работу. Кроме того, в трекере жили не только инженерные задачи, и попытка надёжно отделить одно от другого потребовала бы слишком сложной и хрупкой логики. Поэтому в качестве опоры взяли именно merge request как сущность, гораздо ближе привязанную к реальному изменению в коде.

Обе версии метрики мы считали в двух разрезах — среднем и медианном. Среднее значение помогало замечать общие сдвиги на небольших промежутках времени, а медиана — видеть более устойчивую картину и не теряться из-за единичных выбросов.

Дальше начиналась уже самая сложная часть: чтобы посчитать Lead Time, нужно было понять, какие именно merge request’ы вошли в конкретный релиз.

Если проект релизился через основную ветку или через конкретный образ, который выкатывался в прод, задача была сравнительно прямолинейной. Мы брали разницу между состояниями двух релизов через GitLab API, выделяли merge request’ы, проверяли их связь с задачами и дальше считали, сколько времени каждое изменение шло до прода — и по CMR, и по MMR.

Здесь нам сильно помогало еще одно внутреннее соглашение: ветки в компании обычно называются с номера задачи в трекере. За счет этого merge request было проще сопоставлять с конкретной задачей, а заодно становилось проще видеть состояние изменений не только разработчикам, но и менеджерам продукта.

Но как только заканчивался сценарий «основная ветка → прод», начинались нюансы. Если использовалась релизная ветка, уже было недостаточно просто сравнить два состояния основной ветки: нужно было понять, какие коммиты вошли именно в релизную ветку, и по ним восстановить набор merge request’ов. В сценарии, похожем на git flow, добавлялась еще одна развилка: изменения жили в develop, а релиз фактически происходил при попадании в master.

То есть проблема была в том, чтобы для каждого типа релизного процесса корректно восстановить один и тот же смысл: какие изменения реально доехали до прода в этом релизе.

В итоге все эти варианты пришлось свести в один алгоритм. Для каждого проекта сервис определял его релизный сценарий, выбирал нужный способ получения коммитов и merge request’ов, а затем уже применял одинаковую логику расчёта CMR и MMR.

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

def build_release_data(
    team_name: str,
    project,
    features: Optional[list] = None,
    release_time: Optional[dt.datetime] = None,
    prod_version: Optional[str] = None
) -> Release:
    """
    Расчет средних и медианных значений, создание release_data
    """
    avg_time_cmr = dt.timedelta(0)
    avg_time_mmr = dt.timedelta(0)
    median_time_cmr = dt.timedelta(0)
    median_time_mmr = dt.timedelta(0)

    if features:
        cmr_times = [f.cmr_time.total_seconds() for f in features]
        mmr_times = [f.mmr_time.total_seconds() for f in features]
        avg_time_cmr = dt.timedelta(seconds=sum(cmr_times)/len(cmr_times))
        avg_time_mmr = dt.timedelta(seconds=sum(mmr_times)/len(mmr_times))
        median_time_cmr = dt.timedelta(seconds=median(cmr_times))
        median_time_mmr = dt.timedelta(seconds=median(mmr_times))

    data = {
        "datetime": release_time,
        "prod_version": prod_version,
        "average_time_cmr": avg_time_cmr,
        "average_time_mmr": avg_time_mmr,
        "median_time_cmr": median_time_cmr,
        "median_time_mmr": median_time_mmr,
        "features": features,
        # другие нужные поля...
    }
    return Release(team=team_name, project=project.name, **data)

3. Change Failure Rate: как мы связывали инцидент с релизом

По сравнению с предыдущими метриками Change Failure Rate оказалась заметно проще в расчете и заметно сложнее в подготовке данных. Здесь основная задача была в том, чтобы корректно связать инцидент с релизом, который его вызвал.

Для расчета CFR нам было важно зафиксировать три вещи:

  • какая команда выкатилa проблемный релиз;

  • какие сервисы или команды от него пострадали;

  • какой именно релиз считать причиной инцидента.

Часть этих данных уже была в корпоративном трекере. Информация о команде-виновнике и о пострадавших командах там фиксировалась, поэтому основной вопрос был в атрибуции конкретного релиза. В качестве базового правила мы выбрали последний релиз команды, ответственной за инцидент. Этого хватало в большинстве случаев, а для поздно проявившихся или нестандартных сбоев оставили возможность вручную указать нужный релиз.

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

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

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

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

4. Mean Time to Recovery: сколько времени заняло восстановление

MTTR логически продолжает CFR. Если в предыдущей метрике мы фиксировали сам факт того, что релиз привел к сбою, то здесь уже смотрели, сколько времени заняло восстановление пострадавшего сервиса.

Для этого нужно было понять две точки:

  • когда проблема начала влиять на сервис;

  • когда сервис вернулся в рабочее состояние.

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

В результате MTTR считался не как абстрактное «инцидент закрыт», а как время фактического восстановления сервиса. Это позволяло видеть, сколько минут или часов изменение действительно влияло на пользователя.

Побочным эффектом этой же модели данных стала возможность оценивать и потери от инцидентов по отдельным сервисам. Эти расчёты уже не входили напрямую в DORA-контур и делались отдельно, но сами данные для них появлялись из той же связки релизов, инцидентов и времени восстановления.

Визуализация: как метрики стали рабочим инструментом

После того как мы собрали контур данных и загрузили исторические релизы, следующим этапом стала визуализация в Grafana. Базу данных мы подключили к ней напрямую, а отображение настраивали через SQL-запросы. Это позволяло автоматически подтягивать новые данные и довольно быстро дорабатывать сами графики по мере появления новых запросов.

Не просто дашборд: как DORA-метрики помогли нам сократить инциденты на 80% - 7

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

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

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

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

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

За полгода у нас появился сервис, который собирал, рассчитывал и сохранял DORA-метрики для компании. При этом решение получилось не одноразовым, а расширяемым: без крупных переделок кода его удалось подстроить под разные процессы релиза и разные команды. На этапе MVP мы покрыли 90% проектов. В оставшиеся 10% вошли сервисы, которые деплоятся очень редко, и аналитические системы, где пользовательский контур выражен слабее. 

Но самый интересный эффект начался уже после запуска. Одна из команд довольно быстро увидела на графиках, что ее главная проблема — не качество кода как таковое, а задержки релизов. У команды был собственный релизный график, но со временем о нем просто начали забывать. После этого процесс автоматизировали и добавили напоминания о релизах. В результате показатель MMR снизился с почти двух недель до двух дней. А вместе с этим стабилизировался и сам прод: версия сервиса перестала надолго отставать, изменения перестали копиться, стало меньше конфликтов, а хотфиксы стало проще выкатывать, потому что в релизной ветке не накапливался лишний хвост изменений.

Не просто дашборд: как DORA-метрики помогли нам сократить инциденты на 80% - 8

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

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

К 2026 году количество критичных сбоев по последним данным существенно снизилось:

  • С февраля 2025 по февраль 2026 — на 40%

  • С марта 2025 по март 2026 — на 80%

Если коротко, то главный вывод для меня здесь не в том, что мы «внедрили DORA». Работать начинает связка из нескольких вещей: доверенный контур данных, понятная визуализация, регулярный разбор и готовность менять процессы там, где метрики показывают реальное узкое место.

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


TG-канал Ostrovok! Tech

Автор: sOln1ssshkO

Источник

Оставить комментарий