好的,咱们今天就来聊聊 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;
}
这段代码做了什么?
- 初始化 OpenTelemetry: 配置 Resource、SpanExporter、Processor 和 TracerProvider。Resource 描述了你的服务,SpanExporter 负责将 Span 导出到后端,Processor 负责处理 Span,TracerProvider 负责创建 Tracer。
- 获取 Tracer: 通过 TracerProvider 获取 Tracer。Tracer 用于创建 Span。
- 创建 Span: 通过 Tracer 创建 Span。Span 代表一个操作或步骤。
- 设置 Span 的属性: 通过 Span 的
SetAttribute()
方法设置 Span 的属性。属性用于描述 Span 的额外信息。 - 添加 Event: 通过 Span 的
AddEvent()
方法添加 Event。Event 用于记录 Span 中发生的事件。 - 结束 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;
}
这段代码做了什么?
- 创建 Logger: 通过
spdlog::stdout_color_mt()
创建一个控制台 Logger。 - 设置日志级别: 通过 Logger 的
set_level()
方法设置日志级别。只有级别高于或等于设置级别的日志才会被记录。 - 记录日志: 通过 Logger 的
debug()
,info()
,warn()
,error()
和critical()
方法记录不同级别的日志。 - 使用格式化字符串: 可以使用
{}
作为占位符,将变量的值插入到日志消息中。 - 记录异常: 可以使用
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 做了什么?
- 继承
spdlog::sinks::base_sink
: 继承spdlog
的基类 Sink。 - 重写
sink_before_format()
方法: 在格式化日志消息之前调用。我们在这个方法中获取当前的 Span,并将日志消息作为 Event 添加到 Span 中。 - 获取当前 Span: 通过
trace_api::GetSpan(trace_api::GetCurrentContext())
获取当前的 Span。 - 添加 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;
}
这段代码做了什么?
- 创建自定义 Sink: 创建
OpenTelemetrySinkMt
实例。 - 创建 Logger: 创建一个
spdlog::logger
实例,并将控制台 Sink 和自定义 Sink 添加到 Logger 中。 - 创建 Span: 通过 OpenTelemetry 的 Tracer 创建 Span。
- 创建 Scope: 创建一个
trace_api::Scope
实例,并将 Span 设置为当前的 Context。 - 记录日志: 通过 Logger 记录日志。由于我们使用了自定义 Sink,所以日志消息会被作为 Event 添加到 Span 中。
- 结束 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
集成在一起,我们可以打造一个既强大又易用的监控体系,让你的程序在云端也能被你看得清清楚楚,明明白白。这样,你就可以及时发现问题,快速定位问题,并最终解决问题,让你的程序更加稳定可靠。
记住,好的监控就像给程序穿上了防弹衣,让你在遇到问题的时候,也能从容应对,最终成为一个合格的云原生开发者。
希望今天的分享对你有所帮助!下次再见!