C++ 分布式追踪与日志:OpenTelemetry 与 `spdlog` 集成

好的,咱们今天就来聊聊 C++ 分布式追踪和日志,以及如何把 OpenTelemetry 和 spdlog 这两个好兄弟捏合在一起,打造一个既强大又易用的监控体系。说白了,就是让你的程序在云端裸奔的时候,也能被你看得清清楚楚,明明白白。

开场白:程序裸奔的那些事儿

想象一下,你辛辛苦苦写的程序,终于部署上线了。结果呢?程序就像断了线的风筝,飞到云里雾里,你根本不知道它在干嘛。出问题了,只能抓瞎,对着日志一顿猛翻,效率低到爆。这就像你把孩子丢在茫茫人海,然后指望他自己找到回家的路一样,太难了!

所以,我们需要给程序穿上衣服,让它留下痕迹,告诉我们它的一举一动。这就是分布式追踪和日志的意义所在。

第一部分:OpenTelemetry:追踪界的瑞士军刀

OpenTelemetry (简称 OTel) 是一个 CNCF 的项目,它提供了一套标准的 API、SDK 和工具,用于生成、收集、处理和导出遥测数据,包括追踪 (Traces)、指标 (Metrics) 和日志 (Logs)。

为什么选择 OpenTelemetry?

  • 标准化: 统一的规范,避免厂商锁定,方便切换不同的后端。就像你买了不同品牌的插座,都能用同一个插头一样。
  • 可观测性三剑客: 追踪、指标、日志,一次性解决所有问题。
  • 生态完善: 社区活跃,支持多种语言和框架,方便集成。
  • 厂商中立: 可以对接各种后端,例如 Jaeger、Zipkin、Prometheus 等。

OpenTelemetry 的核心概念

  • Trace (追踪): 一个完整的请求链路,例如从用户发起请求到数据库返回数据的整个过程。
  • Span (跨度): Trace 中的一个独立的操作或步骤,例如一个函数调用、一个 HTTP 请求或一个数据库查询。
  • Context (上下文): 用于在不同的 Span 之间传递信息,例如请求 ID、用户 ID 等。
  • Attributes (属性): 用于描述 Span 的额外信息,例如 HTTP 状态码、数据库查询语句等。
  • Baggage (行李): 一种机制,允许在 Trace 中携带用户自定义的数据,例如用户信息、产品信息等。

代码示例:用 OpenTelemetry 追踪你的 C++ 代码

首先,你需要安装 OpenTelemetry 的 C++ SDK。如果你用的是 vcpkg,可以这样:

vcpkg install opentelemetry

然后,来一段简单的代码:

#include <iostream>
#include <opentelemetry/sdk/trace/tracer_provider.h>
#include <opentelemetry/trace/provider.h>
#include <opentelemetry/exporters/ostream/span_exporter.h>
#include <opentelemetry/sdk/resource/resource.h>
#include <opentelemetry/sdk/trace/samplers/always_on.h>

namespace trace_api = opentelemetry::trace;
namespace trace_sdk = opentelemetry::sdk::trace;
namespace resource  = opentelemetry::sdk::resource;

int main() {
  // 1. 配置 Resource,描述你的服务
  auto resource_attributes = resource::ResourceAttributes{
      {"service.name", "my-cool-service"},
      {"service.version", "1.0.0"}
  };
  auto resource_ = resource::Resource::Create(resource_attributes);

  // 2. 配置 SpanExporter,用于将 Span 导出到后端
  auto exporter  = std::unique_ptr<trace_sdk::SpanExporter>(
      new opentelemetry::exporter::ostream::OStreamSpanExporter);

  // 3. 配置 Processor,用于处理 Span
  auto processor = std::unique_ptr<trace_sdk::SpanProcessor>(
      new trace_sdk::SimpleSpanProcessor(std::move(exporter)));

  // 4. 配置 TracerProvider,用于创建 Tracer
  auto provider = std::shared_ptr<trace_api::TracerProvider>(
      new trace_sdk::TracerProvider(std::move(processor), resource_));

  // 设置全局 TracerProvider
  trace_api::Provider::SetTracerProvider(provider);

  // 5. 获取 Tracer
  auto tracer = provider->GetTracer("my-tracer", "1.0");

  // 6. 创建 Span
  auto span = tracer->StartSpan("my-span");

  // 7. 设置 Span 的属性
  span->SetAttribute("http.method", "GET");
  span->SetAttribute("http.url", "https://example.com");

  // 8. 添加 Event
  span->AddEvent("my-event", {{"key", "value"}});

  // 9. 模拟一些操作
  std::cout << "Doing something..." << std::endl;

  // 10. 结束 Span
  span->End();

  // 为了确保exporter刷新,等待一段时间
  std::this_thread::sleep_for(std::chrono::seconds(1));

  return 0;
}

这段代码做了什么?

  1. 初始化 OpenTelemetry: 配置 Resource、SpanExporter、Processor 和 TracerProvider。Resource 描述了你的服务,SpanExporter 负责将 Span 导出到后端,Processor 负责处理 Span,TracerProvider 负责创建 Tracer。
  2. 获取 Tracer: 通过 TracerProvider 获取 Tracer。Tracer 用于创建 Span。
  3. 创建 Span: 通过 Tracer 创建 Span。Span 代表一个操作或步骤。
  4. 设置 Span 的属性: 通过 Span 的 SetAttribute() 方法设置 Span 的属性。属性用于描述 Span 的额外信息。
  5. 添加 Event: 通过 Span 的 AddEvent() 方法添加 Event。Event 用于记录 Span 中发生的事件。
  6. 结束 Span: 通过 Span 的 End() 方法结束 Span。

运行这段代码,你会在控制台看到 Span 的信息。当然,这只是一个简单的示例。在实际项目中,你需要将 Span 导出到后端,例如 Jaeger 或 Zipkin。

第二部分:spdlog:日志界的劳模

spdlog 是一个非常快速的 C++ 日志库。它简单易用,性能优秀,支持多种日志级别和格式化选项。

为什么选择 spdlog

  • 性能: spdlog 的性能非常出色,可以满足高并发场景的需求。
  • 易用性: spdlog 的 API 非常简单易懂,上手容易。
  • 灵活性: spdlog 支持多种日志级别和格式化选项,可以根据需要进行配置。
  • 可扩展性: spdlog 支持自定义 Sink,可以将日志输出到不同的目标,例如文件、控制台、网络等。

代码示例:用 spdlog 记录日志

首先,你需要安装 spdlog。如果你用的是 vcpkg,可以这样:

vcpkg install spdlog

然后,来一段简单的代码:

#include <iostream>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h> // 包含彩色控制台sink

int main() {
  // 创建一个控制台logger
  auto console_logger = spdlog::stdout_color_mt("console");

  // 设置日志级别
  console_logger->set_level(spdlog::level::debug);

  // 记录不同级别的日志
  console_logger->debug("This is a debug message");
  console_logger->info("This is an info message");
  console_logger->warn("This is a warning message");
  console_logger->error("This is an error message");
  console_logger->critical("This is a critical message");

  // 使用格式化字符串
  console_logger->info("The answer is {}", 42);

  // 记录异常
  try {
    throw std::runtime_error("Something went wrong");
  } catch (const std::exception& e) {
    console_logger->error("Exception caught: {}", e.what());
  }

  return 0;
}

这段代码做了什么?

  1. 创建 Logger: 通过 spdlog::stdout_color_mt() 创建一个控制台 Logger。
  2. 设置日志级别: 通过 Logger 的 set_level() 方法设置日志级别。只有级别高于或等于设置级别的日志才会被记录。
  3. 记录日志: 通过 Logger 的 debug(), info(), warn(), error()critical() 方法记录不同级别的日志。
  4. 使用格式化字符串: 可以使用 {} 作为占位符,将变量的值插入到日志消息中。
  5. 记录异常: 可以使用 error() 方法记录异常信息。

运行这段代码,你会在控制台看到不同级别的日志消息。

第三部分:OpenTelemetry + spdlog:强强联合,天下无敌

现在,我们来把 OpenTelemetry 和 spdlog 集成在一起,让我们的程序既能追踪,又能记录日志。

思路:自定义 spdlog Sink,将日志消息作为 OpenTelemetry Event 添加到 Span 中

我们可以创建一个自定义的 spdlog Sink,将日志消息作为 OpenTelemetry Event 添加到当前的 Span 中。这样,我们就可以在追踪系统中看到程序的日志信息。

代码示例:自定义 spdlog Sink

#include <spdlog/sinks/base_sink.h>
#include <opentelemetry/trace/span.h>
#include <opentelemetry/trace/tracer.h>
#include <opentelemetry/trace/provider.h>

namespace trace_api = opentelemetry::trace;

template<typename Mutex>
class OpenTelemetrySink : public spdlog::sinks::base_sink<Mutex> {
public:
  explicit OpenTelemetrySink() {}

protected:
  void sink_before_format(spdlog::details::log_msg& msg) override {
    // 在格式化之前获取 Span
    auto current_span = trace_api::GetSpan(trace_api::GetCurrentContext());
    if (!current_span) {
      return; // 如果没有 Span,则不添加 Event
    }

    // 构建 Event 的属性
    opentelemetry::common::KeyValueIterable attrs = {
        {"level", spdlog::level::to_str(msg.level)}
    };

    // 添加 Event 到 Span
    current_span->AddEvent(msg.payload, attrs);
  }

  void sink_after_format(const spdlog::details::log_msg& msg) override {
    // 不需要做任何事情
  }
};

using OpenTelemetrySinkMt = OpenTelemetrySink<std::mutex>;
using OpenTelemetrySinkSt = OpenTelemetrySink<spdlog::details::null_mutex>;

这个自定义 Sink 做了什么?

  1. 继承 spdlog::sinks::base_sink: 继承 spdlog 的基类 Sink。
  2. 重写 sink_before_format() 方法: 在格式化日志消息之前调用。我们在这个方法中获取当前的 Span,并将日志消息作为 Event 添加到 Span 中。
  3. 获取当前 Span: 通过 trace_api::GetSpan(trace_api::GetCurrentContext()) 获取当前的 Span。
  4. 添加 Event 到 Span: 通过 Span 的 AddEvent() 方法添加 Event。我们将日志消息作为 Event 的名称,将日志级别作为 Event 的属性。

代码示例:使用自定义 Sink

#include <iostream>
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include "opentelemetry_sink.h" // 包含自定义 Sink 的头文件

#include <opentelemetry/sdk/trace/tracer_provider.h>
#include <opentelemetry/trace/provider.h>
#include <opentelemetry/exporters/ostream/span_exporter.h>
#include <opentelemetry/sdk/resource/resource.h>
#include <opentelemetry/sdk/trace/samplers/always_on.h>

namespace trace_api = opentelemetry::trace;
namespace trace_sdk = opentelemetry::sdk::trace;
namespace resource  = opentelemetry::sdk::resource;

int main() {
    // 1. 配置 Resource,描述你的服务
    auto resource_attributes = resource::ResourceAttributes{
        {"service.name", "my-cool-service"},
        {"service.version", "1.0.0"}
    };
    auto resource_ = resource::Resource::Create(resource_attributes);

    // 2. 配置 SpanExporter,用于将 Span 导出到后端 (这里使用 OStreamSpanExporter)
    auto exporter  = std::unique_ptr<trace_sdk::SpanExporter>(
        new opentelemetry::exporter::ostream::OStreamSpanExporter);

    // 3. 配置 Processor,用于处理 Span
    auto processor = std::unique_ptr<trace_sdk::SpanProcessor>(
        new trace_sdk::SimpleSpanProcessor(std::move(exporter)));

    // 4. 配置 TracerProvider,用于创建 Tracer
    auto provider = std::shared_ptr<trace_api::TracerProvider>(
        new trace_sdk::TracerProvider(std::move(processor), resource_));

    // 设置全局 TracerProvider
    trace_api::Provider::SetTracerProvider(provider);

    // 5. 获取 Tracer
    auto tracer = provider->GetTracer("my-tracer", "1.0");

  // 创建一个控制台logger
  auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
  // 创建 OpenTelemetry sink
  auto otel_sink = std::make_shared<OpenTelemetrySinkMt>();

  // 创建一个 multi_logger,同时使用控制台 sink 和 OpenTelemetry sink
  spdlog::logger logger("my_logger", {console_sink, otel_sink});
  logger.set_level(spdlog::level::debug);

  // 6. 创建 Span
  auto span = tracer->StartSpan("my-span");
  auto scope = trace_api::Scope(span); // 创建一个 Scope,将 Span 设置为当前 Context

  // 7. 使用 logger 记录日志
  logger.debug("This is a debug message within a span");
  logger.info("This is an info message within a span");
  logger.warn("This is a warning message within a span");
  logger.error("This is an error message within a span");

  // 8. 结束 Span
  span->End();

  // 为了确保exporter刷新,等待一段时间
  std::this_thread::sleep_for(std::chrono::seconds(1));

  return 0;
}

这段代码做了什么?

  1. 创建自定义 Sink: 创建 OpenTelemetrySinkMt 实例。
  2. 创建 Logger: 创建一个 spdlog::logger 实例,并将控制台 Sink 和自定义 Sink 添加到 Logger 中。
  3. 创建 Span: 通过 OpenTelemetry 的 Tracer 创建 Span。
  4. 创建 Scope: 创建一个 trace_api::Scope 实例,并将 Span 设置为当前的 Context。
  5. 记录日志: 通过 Logger 记录日志。由于我们使用了自定义 Sink,所以日志消息会被作为 Event 添加到 Span 中。
  6. 结束 Span: 通过 Span 的 End() 方法结束 Span。

运行这段代码,你会在控制台看到日志消息,并且你会在追踪系统中看到 Span 的 Event,其中包含了日志消息和日志级别。

表格总结:OpenTelemetry vs. spdlog

特性 OpenTelemetry spdlog
功能 分布式追踪、指标、日志 日志记录
范围 整个请求链路 单个服务或组件
用途 监控系统整体性能和问题定位 记录程序运行时的状态和错误信息
核心概念 Trace, Span, Context, Attributes, Baggage Logger, Sink, Level, Formatter
集成方式 通过 API 和 SDK 集成 通过自定义 Sink 集成

最佳实践

  • 选择合适的日志级别: 根据实际情况选择合适的日志级别,避免记录过多的无用信息。
  • 使用结构化日志: 使用结构化日志可以方便地进行日志分析和查询。
  • 为 Span 添加有意义的属性: 为 Span 添加有意义的属性可以帮助你更好地理解请求链路。
  • 使用 Baggage 传递上下文信息: 使用 Baggage 可以将用户自定义的数据传递到下游服务。
  • 选择合适的后端: 根据实际需求选择合适的 OpenTelemetry 后端,例如 Jaeger、Zipkin 或 Prometheus。

总结:让你的程序不再裸奔

通过将 OpenTelemetry 和 spdlog 集成在一起,我们可以打造一个既强大又易用的监控体系,让你的程序在云端也能被你看得清清楚楚,明明白白。这样,你就可以及时发现问题,快速定位问题,并最终解决问题,让你的程序更加稳定可靠。

记住,好的监控就像给程序穿上了防弹衣,让你在遇到问题的时候,也能从容应对,最终成为一个合格的云原生开发者。

希望今天的分享对你有所帮助!下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注