





















Opentelemetry-cpp 是可观测领域,opentelemetry (CNCF基金会孵化项目)的C++ SDK接入层。 opentelemetry 里面主要是分链路跟踪(Trace)、指标(Metrics)、日志(Logs)三大块。 同时 opentelemetry 有一个标准规范文档 opentelemetry-specification ,而SDK实现主要就是来对这个标准规范文档的特定语言实现。 由于日志(Logs)这一块一直处于Experimental阶段,所以很长时间以来 C++ SDK接入层 都没有及时更新跟进规范的变化。
去年底的时候,我抽时间来更新了一波规范实现。但是拖到今天才来写这篇分享( * T _ T * ),也是想着把一些向前不兼容的变化列举出来,方便可能有些同学升级的时候可能会碰到一些问题可以对照解决。
首先简单介绍一下 opentelemetry-cpp 的内部模块结构,主要是三部分: API, SDK, Exporter 。然后还有其他一些扩展和辅助性功能。
在 Logs的规范 层面,大部分的变化只是名称上的变化,还有少量的功能性增强和一些仍然处于非常早期的新功能草案。
首先,规范定义了一个 LogRecord 类型,并且要求在API组件中实现的导出接口全部以这个 LogRecord 为基准,LogRecord 要求实现所有字段的 setter 接口。
同时,导出接口 从原来的 Logger::Log(...) 重命名为了 Logger::EmitLogRecord(...) ,并且在继承关系中,仅需要实现 Logger::EmitLogRecord(nostd::unique_ptr<LogRecord>) 。
这部分的变化,由于原先的 SDK模块中也有个 LogRecord 结构,这里其实是不同含义,但是重名了。所以我们废弃了 SDK中的 LogRecord 。
新的规范中在SDK里也加入了 ReadableLogRecord 和 ReadWriteLogRecord 两个接口,要求 ReadableLogRecord 必须实现 getter 接口,而 ReadWriteLogRecord setter 和 getter 都有。
其实这里的 ReadWriteLogRecord 就很像之前 SDK 里的 LogRecord 。
另外由于SDK里所有模块的数据传递都是通过各自内部的一个叫 Recordable 的对象,并且这个对象还会包含一些SDK内的独有的内容,比如Resource和InstrumentationScope。
所以我们最终的继承关系改成了如下所示:
classDiagram LogRecord <|– Recordable Recordable <|– ReadableLogRecord Recordable <|– 特定Exporter的Recordable ReadableLogRecord <|– ReadWriteLogRecord LogRecord: +SetTimestamp() LogRecord: +SetObservedTimestamp() LogRecord: +SetSeverity() LogRecord: +SetBody() LogRecord: +SetAttribute() LogRecord: +SetTraceId() LogRecord: +SetSpanId() LogRecord: +SetTraceFlags() class Recordable { +SetResource() +SetInstrumentationScope() } class ReadableLogRecord { GetTimestamp() GetObservedTimestamp() GetSeverity() GetSeverityText() GetBody() GetTraceId() GetSpanId() GetTraceFlags() GetAttributes() GetResource() GetInstrumentationScope() } class 特定Exporter的Recordable { 位于Exporter中,比如 OtlpLogRecordable }
另外所有的 Processor、Exporter、Factory的名字都由 *Log*Processor , *Log*Exporter 和 *Log*Factory 变为了 *LogRecord*Processor , *LogRecord*Exporter 和 *LogRecord*Factory 。
对应的头文件的文件名也做了相应修改。
对于 Processor 接口, OnReceive 重命名为了 OnEmit 。
我们还移除了老 Logger::Log(...) 接口中的 name 字段。在非常古老的版本中,Logs的协议里有 name 字段,所以 Logger::Log(...) 接口里也有个 name 参数。
但是实际上很早期的一次协议更新就已经移除这个字段了(从v1.4.0版本开始),我们在接口层保留了相当长的时间,并且设置为了 deprecated 就是为了给用户一段时间去迁移。
现在这次改造就顺带移除了这个废弃的接口,以防误用。
API接口中一些STL类型的接口换成了 nostd 的版本,比如 std::unique_ptr -> nostd::unique_ptr 。这部分主要是和其他模块保持一致,管理ABI兼容性。
规范变化也包含一些新功能,比如说,我们现在可以通过 LoggerProvider::GetLogger() 接口来设置自动从链路跟踪(Trace)模块的 RuntimeContext 中自动提取和关联当前的调用链路信息。
这样可以把日志自动关联到链路上,当然这需要使用链路跟踪(Trace)模块的模块启用里面的 Scope 组件。我们仍然支持手动设置链路信息。
Logs的数据字段方面,增加了一个 observed_timestamp ,这个字段会默认填写为上报创建 LogRecord 时的时间。当然也是可以手动设置的。
我们现在也允许在 LoggerProvider::GetLogger() 创建Logger的时候指定InstrumentationScope的属性,不过目前的版本中只是接口合入了,下个Release才会真正进入到Exporter的数据中。
详见: https://github.com/open-telemetry/opentelemetry-cpp/pull/2004 这个PR的合入进展。
新的规范有一个还处于非常初期的 EventLog 的草案,主要是包装了一下普通版本的 Logger ,在要设置log的关联事件的时候规范化 event.name 和 event.domain 的使用。这个可能未来还会变化比较大,暂时不推荐使用。
由于新增了一些属性,Logs的一键调用接口(包括 EmitLogRecord, Debug, Error, Info 等)的参数越多了。并且大部分都是可选的,这对接口的维护带来了极大得不变。为了缓解这个问题,我用type traits的方式重构了这部分的重载。相当于把原来的按类型+参数顺序的重载改成了仅仅按类型的重载,无所谓传参的顺序。然后根据参数类型来决定如何填充字段。比如参考其中一个接口注释:
/**
* Emit a Log Record object with arguments
*
* @param log_record Log record
* @tparam args Arguments which can be used to set data of log record by type.
* Severity -> severity, severity_text
* string_view -> body
* AttributeValue -> body
* SpanContext -> span_id,tace_id and trace_flags
* SpanId -> span_id
* TraceId -> tace_id
* TraceFlags -> trace_flags
* SystemTimestamp -> timestamp
* system_clock::time_point -> timestamp
* KeyValueIterable -> attributes
* Key value iterable container -> attributes
* span<pair<string_view, AttributeValue>> -> attributes(return type of MakeAttributes)
*/
template <class... ArgumentType>
void EmitLogRecord(nostd::unique_ptr<LogRecord> &&log_record, ArgumentType &&... args);
这上面列出了如何按参数类型会影响哪些字段。那么如何实现呢?首先由于传入的参数 ArgumentType 是个通用引用,它既能匹配左值引用,又能匹配右值,还能匹配是否带 const 和 violate ,所以第一层模板类型提取我们可以用 std::decay<ArgumentType>::type 来移除这些类型修饰。然后通过一个特殊类型的特化来实现不同的功能,参数传递走完美转发就可以了。比如:
template <>
struct LogRecordSetterTrait<trace::SpanContext>
{
template <class ArgumentType>
inline static LogRecord *Set(LogRecord *log_record, ArgumentType &&arg) noexcept
{
log_record->SetSpanId(arg.span_id());
log_record->SetTraceId(arg.trace_id());
log_record->SetTraceFlags(arg.trace_flags());
return log_record;
}
};
template <>
struct LogRecordSetterTrait<trace::SpanId>
{
template <class ArgumentType>
inline static LogRecord *Set(LogRecord *log_record, ArgumentType &&arg) noexcept
{
log_record->SetSpanId(std::forward<ArgumentType>(arg));
return log_record;
}
};
可以看到,如果参数类型是 trace::SpanContext 我们会同时设置 trace_id 、 span_id 和 trace_flags 。但是如果传入的是 trace::SpanId 我们就只会设置 trace_id 。
这里还有一处和之前不太兼容的特殊的地方,就是通用引用(Universal Reference),就是 ArgumentType && 不能匹配 std::initializer_list<T> 。而我们之前是可以通过 std::initializer_list<std::pair<nostd::string_view, common::AttributeValue>> 来传递attributes的。
这里为了适配,我们提供了一组 MakeAttributes(...) 接口,通过重载来适配attributes的传递。整个实现大概是这样:
inline static nostd::span<const std::pair<nostd::string_view, common::AttributeValue>>
MakeAttributes(std::initializer_list<std::pair<nostd::string_view, common::AttributeValue>>
attributes) noexcept
{
return nostd::span<const std::pair<nostd::string_view, common::AttributeValue>>{
attributes.begin(), attributes.end()};
}
inline static nostd::span<const std::pair<nostd::string_view, common::AttributeValue>>
MakeAttributes(
nostd::span<const std::pair<nostd::string_view, common::AttributeValue>> attributes) noexcept
{
return attributes;
}
inline static const common::KeyValueIterable &MakeAttributes(
const common::KeyValueIterable &attributes) noexcept
{
return attributes;
}
template <
class ArgumentType,
nostd::enable_if_t<common::detail::is_key_value_iterable<ArgumentType>::value> * = nullptr>
inline static common::KeyValueIterableView<ArgumentType> MakeAttributes(
const ArgumentType &arg) noexcept
{
return common::KeyValueIterableView<ArgumentType>(arg);
}
使用起来可以类似这种 logger->EmitLogRecord(Severity::kError, MakeAttributes({ {"key1", "value 1"}, {"key2", 2} })); 或 logger->Debug("Test log message", MakeAttributes(std::map<std::string, std::string>())); 。应该也不会造成太大困扰。
主要的改动也就这么多,也欢迎有兴趣的小伙伴们互相交流。 如有遗漏,也欢迎小伙伴们指出和补充。
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。