前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Opentelemetry-cpp的Logs模块标准更新

Opentelemetry-cpp的Logs模块标准更新

作者头像
owent
发布2023-03-06 11:58:56
5330
发布2023-03-06 11:58:56
举报
文章被收录于专栏:owent

背景

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里也加入了 ReadableLogRecordReadWriteLogRecord 两个接口,要求 ReadableLogRecord 必须实现 getter 接口,而 ReadWriteLogRecord setter 和 getter 都有。 其实这里的 ReadWriteLogRecord 就很像之前 SDK 里的 LogRecord 。 另外由于SDK里所有模块的数据传递都是通过各自内部的一个叫 Recordable 的对象,并且这个对象还会包含一些SDK内的独有的内容,比如Resource和InstrumentationScope。 所以我们最终的继承关系改成了如下所示:

代码语言:javascript
复制
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.nameevent.domain 的使用。这个可能未来还会变化比较大,暂时不推荐使用。

由于新增了一些属性,Logs的一键调用接口(包括 EmitLogRecord, Debug, Error, Info 等)的参数越多了。并且大部分都是可选的,这对接口的维护带来了极大得不变。为了缓解这个问题,我用type traits的方式重构了这部分的重载。相当于把原来的按类型+参数顺序的重载改成了仅仅按类型的重载,无所谓传参的顺序。然后根据参数类型来决定如何填充字段。比如参考其中一个接口注释:

代码语言:javascript
复制
/**
  * 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 是个通用引用,它既能匹配左值引用,又能匹配右值,还能匹配是否带 constviolate ,所以第一层模板类型提取我们可以用 std::decay<ArgumentType>::type 来移除这些类型修饰。然后通过一个特殊类型的特化来实现不同的功能,参数传递走完美转发就可以了。比如:

代码语言:javascript
复制
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的传递。整个实现大概是这样:

代码语言:javascript
复制
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>())); 。应该也不会造成太大困扰。

总结

主要的改动也就这么多,也欢迎有兴趣的小伙伴们互相交流。 如有遗漏,也欢迎小伙伴们指出和补充。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2023-02-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 不兼容变更(Break changes)
    • 命名变更
    • 新增功能
    • 总结
    相关产品与服务
    日志服务
    日志服务(Cloud Log Service,CLS)是腾讯云提供的一站式日志服务平台,提供了从日志采集、日志存储到日志检索,图表分析、监控告警、日志投递等多项服务,协助用户通过日志来解决业务运维、服务监控、日志审计等场景问题。
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档