背景
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 。然后还有其他一些扩展和辅助性功能。
- API模块: 主要用于各类组件接入创建链路跟踪(Trace)、指标(Metrics)、日志(Logs),这部分必须是Head-Only的,ABI稳定的。
- SDK模块: 主要用于应用框架层来接入如何实际产生和处理数据的实现层,和API模块搭配可以做出类似热插拔的效果。
- Exporter模块: 决定如何导出数据,用什么协议导出。
- 其他: 包含HTTP封装,插件模块等周边支持组件
不兼容变更(Break changes)
在 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>()));
。应该也不会造成太大困扰。
总结
主要的改动也就这么多,也欢迎有兴趣的小伙伴们互相交流。 如有遗漏,也欢迎小伙伴们指出和补充。