Загляни внутрь своего 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);
// Симуляция запросов