Observability в C++: трейсинг, метрики и логи для профессионалов

Онлайн-тренажер Python для начинающих

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

Начать курс

Загляни внутрь своего C++ кода: почему observability — это не роскошь, а необходимость

Представьте: вы запустили высоконагруженный сервис на C++. Прошёл час, два, день — всё летает. А потом — бац! — пользователи жалуются на тормоза, а вы сидите и гадаете: где узкое место? В памяти? В сети? Или просто кто-то забыл освободить ресурс? Без системы observability вы — как капитан корабля с заклеенными иллюминаторами. Вы чувствуете, что что-то идёт не так, но не видите — что именно.

Observability (наблюдаемость) в C++ — это ваш набор перископов и радаров. Это не просто логирование, это три кита: трейсинг, метрики и логи. В этой статье мы разберём каждый из них на реальных примерах кода, покажем, как подружить C++ с OpenTelemetry и Prometheus, и дадим готовые рецепты для production. Поехали!

Три столпа observability: трейсинг, метрики, логи

Прежде чем нырять в код, давайте чётко определим, чем эти понятия отличаются и как они дополняют друг друга. Многие путают observability с простым логированием — и зря. Это как сравнивать фонарик с прожекторной установкой.

  • Логи — это события. "Что произошло?" (например, "Пользователь авторизовался" или "Ошибка соединения с БД"). Они детальны, но без контекста могут быть бесполезны.
  • Метрики — это числа. "Сколько?" (количество запросов в секунду, использование CPU, время ответа). Они агрегированы и показывают тренды.
  • Трейсинг — это путь запроса. "Как именно это произошло?" (от входа в сервис до вызова базы данных и ответа клиенту). Это связующее звено между логами и метриками.

Идеальная observability в C++ — это когда вы, глядя на дашборд, можете за 10 секунд понять: где, когда и почему произошла аномалия. Без этого вы будете играть в угадайку с продакшеном.

Трейсинг в C++: как внедрить OpenTelemetry

Начнём с самого мощного, но и самого сложного инструмента — трейсинга. OpenTelemetry — это стандарт де-факто для сбора телеметрии. В C++ он поддерживается не так богато, как в Go или Java, но работает стабильно.

Допустим, у нас есть простой HTTP-сервер на C++ (используем идею, а не конкретную библиотеку). Нам нужно отследить, как долго обрабатывается каждый запрос и какие вызовы внутри него происходят.

Настройка OpenTelemetry в C++

Сначала устанавливаем зависимости. В CMakeLists.txt добавляем:

find_package(opentelemetry-cpp REQUIRED)
target_link_libraries(my_app PRIVATE opentelemetry::api opentelemetry::exporter_ostream)

Теперь создадим трейсер и спаны (span — это единица работы). Вот минимальный пример:

#include <opentelemetry/trace/provider.h>
#include <opentelemetry/exporters/ostream/span_exporter.h>
#include <opentelemetry/sdk/trace/tracer_provider.h>
#include <opentelemetry/sdk/trace/simple_processor.h>

namespace trace_api = opentelemetry::trace;
namespace trace_sdk = opentelemetry::sdk::trace;
namespace trace_exporter = opentelemetry::exporter::trace;

void init_tracer() {
    auto exporter = std::unique_ptr<trace_sdk::SpanExporter>(
        new trace_exporter::OStreamSpanExporter(std::cout));
    auto processor = std::unique_ptr<trace_sdk::SpanProcessor>(
        new trace_sdk::SimpleSpanProcessor(std::move(exporter)));
    auto provider = std::make_shared<trace_sdk::TracerProvider>(std::move(processor));
    trace_api::Provider::SetTracerProvider(provider);
}

void process_request(const std::string& user_id) {
    auto tracer = trace_api::Provider::GetTracerProvider()->GetTracer("my_service");
    auto span = tracer->StartSpan("process_request");
    span->SetAttribute("user.id", user_id);

    // Симуляция работы
    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    span->End();
}

int main() {
    init_tracer();
    process_request("user_42");
    return 0;
}

Этот код выведет в консоль информацию о спане: его имя, атрибуты и время выполнения. В реальном проекте вы будете отправлять трейсы в Jaeger или Zipkin, но для начала и ostream-экспортёр сойдёт.

Важно: не забывайте закрывать спаны (через End или RAII-обёртки). Утечка спанов — частая ошибка новичков.

Практический совет: контекстная передача

В асинхронном коде трейсинг усложняется. Используйте opentelemetry::context::Context для передачи trace_id между потоками или корутинами. Вот пример:

auto span = tracer->StartSpan("async_work");
auto ctx = span->GetContext();
// Передаём ctx в другой поток
std::thread([ctx, tracer]() {
    auto child_span = tracer->StartSpan("child_work", nullptr, ctx);
    // ... работа ...
    child_span->End();
}).join();
span->End();

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

Метрики: Prometheus и C++

Метрики — это ваш пульс. Без них вы не узнаете, что сервис начал задыхаться, пока не станет слишком поздно. Prometheus — стандартный выбор для сбора метрик в современных системах. Клиент для C++ есть, хоть и неофициальный (например, prometheus-cpp).

Счётчики, гистограммы и суммари

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

#include <prometheus/counter.h>
#include <prometheus/histogram.h>
#include <prometheus/registry.h>
#include <prometheus/exposer.h>

using namespace prometheus;

int main() {
    // Создаём реестр метрик
    auto registry = std::make_shared<Registry>();

    // Гистограмма для времени запросов
    auto& histogram_family = BuildHistogram()
        .Name("http_request_duration_seconds")
        .Help("Duration of HTTP requests in seconds")
        .Register(*registry);

    auto& histogram = histogram_family.Add({{"method", "GET"}, {"endpoint", "/api"}});

    // Счётчик запросов
    auto& counter_family = BuildCounter()
        .Name("http_requests_total")
        .Help("Total number of HTTP requests")
        .Register(*registry);

    auto& counter = counter_family.Add({{"method", "GET"}});

    // Экспонируем метрики на порту 8080
    Exposer exposer{"127.0.0.1:8080"};
    exposer.RegisterCollectable(registry);

    // Симуляция запросов                

Блоги

Рекомендаций по книгам