OpenTelemetry
之前学习opentelemetry时整理的内容,仅学习整理,附个人理解,有错指正。转载注明出处。
个人原文blog: http://dopaminer.xyz/2021/06/14/opentelemetry/#more
Opentelemetry
1. OpenTelemetry是用来做什么的
OpenTelemetry合并了openTracing和OpenCensus,它提供了一组API,标准库用来创建和管理遥测数据如traces,metric或者log,通过对这些遥测数据的分析,我们可以进一步的去了解软件的性能和行为。OpenTelemetry提供了一个与厂商无关的实现,因此可以按照自己的需求将其发送到不同的后端进行分析。它支持一些现在一些流行的开源项目,包括Jaeger和Prometheus
2. OpenTelemetry的数据源及内容
2.1. Traces
(1) trace
trace代表什么。首先从一个例子出发,假如小明他今天有如下活动计划:
- 从家里出发,走路去书店
- 到达书店,开始买书等
- 买完书走路去商城
- 去商城购物
- 搭公交车回家
那么小明从家出发,去书店买书,去商城购物,搭公交回家的这整一个过程(一整条流程链,小明今天的活动总和)就可以看过一个trace,即包含了很多个子流程(买书,购物,搭公交车回家等)的流程。
上图是一个简单的trace例子(注意,虽然这条链路跨了两个Service,但它属于一个trace,可以将ServiceA类比小明在书店买书,将ServiceB类比小明在商城购物), 有效的trace由有效的子trace组成。
在上述小明买东西的例子中,我们可以把他一整天的流程看作一个大trace(即这个trace代表小明今天的活动总和),然后他在书店中的具体一系列操作(如看书,买书等构成的在书店买书整个过程)整体看成一个小trace,那么这个大的trace就包含了这个小的trace,这样就可以理解什么是有效的trace由有效的子trace组成。
一个trace
是链接span
的集合,这些span都是表示请求中的工作单元的命名和定时操作。
(2) span
在trace中提到的span指什么?继续用上述小明这个例子。
小明去书店买书,比如他在书店中分别挑选了《C++语言程序设计》,《The Go Programming Language》,《程序猿护发秘籍》,然后去结账,那么小明在书店买书就可以是如下流程(忽略各个动作之间的时间间隔)。
这时候将这个具体操作放入到一开始的图中,如下图
这时候在书店这个小trace里的每一项具体操作就可以看成是一个span,即一个命名(这里命名为"买《C++语言程序设计》")的定时操作。
当然如果图中的操作可以继续往下划分,其中的每一个操作也可以作为一个trace,这里认为已经不可划分了。
进入正题…
span是什么
span
是trace
的构建块,是一个命名的定时操作(即用一个name来表示,表示需要耗费一定时间的操作,即为span。如一次命名的http请求或rpc调用等),它表示分布式系统中的一部分工作流,多个span链接在一起形成一个trace。
实际应用中这个span可以是完整http的请求响应,sql查询,rpc调用等。
如何理解trace和span的关系
trace通常被认为是span的树(tree),它用来反映每个span开始和完成的时间。一个trace包含了一个根span,这个根span囊括了整一个请求从头到尾的延迟(正如上述中的trace代表了小明一整天的的流程链,小trace代表了小明在书店买书的整个流程链)。我们可以把这个trace视为一个单一的逻辑操作。它还显示了span之间的关系。下面是基于上图请求中具体的trace的视图。
如果在上述小明的例子中,它大概是这个意思(不太准确)。
trace开始于一个代表请求开始的root span,这个root span可以有一个或多个的子span,这些子span又可以有它们自己的子span…(子又生孙,孙又生子,子子孙孙无穷匮也…)
span的目的是向可观测性工具提供有关程序执行的一些信息,因此它应该包含一些工作(执行)的详细信息
单个span组件应该包含以下信息:
- 操作名(span name)
- 开始和结束时间戳
- spanContext
- 一个属性set
- 有序事件列表
trace和span的例子
上图中的圆角矩阵长度表示该调用的总时长。
(3) Trace如何跨域传播
不恰当的例子
假设小明是个健忘的人,事情若不记录下来他过一会就会忘得一干二净。
因此把他在书店做的所有事都写进了身上的备忘录,随后小明来到商城,在某家店铺购物途时店主小黑中问他 三点几啦饮咗茶没啊 今天做了什么事,此时小明二话不说直接掏出了心爱的备忘录,然后一五一十的将记录的内容读给小黑听。在这个例子中,小明的备忘录作为介质,把他在书店的动作(信息数据)记录起来并全部带到了商城,实现trace的跨域(书店->商城)传播。此时若小明要记录在商城购物的所有事情,这时候就可以接着之前记录的书店信息继续记录,从而形成一条完整的流程链。
倘若小明不记录在书店做的事情,那么由于他健忘的原因,当他要记录在商城所做事情的时候,就无法接着之前的流程链继续记录。
3.1. context
context
(上下文)是一种传播机制,它可以跨API边界和有逻辑关联的执行单元之间传递执行范围内的值,Cross-cutting concerns
(横切关注点)可以使用共享的相同context对象来访问其处理的数据。
横切关注点指的是一些具有横越多个模块的行为,使用传统的软件开发方法不能够达到有效的模块化的一类特殊关注点。 —《维基百科》
context
必须是不可变的,并且它的写操作必须得返创建一个新的context,这个新context包含原始值和指定更新值。
3.2. propagation
propagation
是一种能让trace变成分布式trace的机制(跨域),并促进context
在服务和进程之间的流动。
context
可以被注入(inject)到一个请求中并由接受服务来提取(extract)并生成新的span
,这个服务可能又会生成新的请求,然后再把context
注入到请求中并发送到其他的服务等等。
这里的注入操作可以类比小明把在书店所做事记录在备忘录。
取出操作可以类比小明把备忘录的内容读给小黑听。
propagation
通常通过特定的请求拦截器(request interceptors)和**传播器(propagator)**来实现,其中interceptors
用来检测传入和传出的请求,并分别使用propagator
的注入和提取操作。
3.3. propagator
propagator
是用来实现propagation
机制的,它被定义于用于在应用程序之间交换message
中读写context
对象。
3.4. carrier
carrier
是propagator
用来读值和写值的介质。每个特定的propagator类型都定义了与其预期的carrier类型,比如string map
或 一个 byte array
类比小明的备忘录,充当记录信息或值的介质。
3.5. propagators的API
在知道propagator和carrier的作用后,我们就可以来看看propagator是如何操作的。
对于propagator
,它必须得实现Inject
和Extract
操作(作用), 这两个操作是为了能够分别地从carriers
中写入和读取数据(如上文中提到的context
,然后下个服务可以从这个context中通过API取出span)。每一个propagator
必须定义其特定的carrier
类型,并且允许定义其他参数。
-
Inject
将值注入到carrier中。例如,注入到一个HTTP请求头中
必备参数:
(1) 一个
Context
,一个propagator必须先从context中检索适当的值,如spanContext,Baggage或其他横切关注context(cross-cutting concern context)(2) 一个
carrier
,这个carrier必须是能够承载propagation字段。例如,一个传输的信息或者HTTP请求。 -
Extract
从传入的请求中提取出值。例如,从HTTP请求头中提取
必备参数:
(1)
context
(2)
carrier
返回一个新的context,这个返回的context是传入的context参数派生出来的。返回的context包含可提取的值,这个值可能是spanContext,Baggage或他横切关注context(cross-cutting concern context)
(4) tracing API
tracing API主要由以下组成:
-
TracerProvier
: API的入口点,它提供了tracers的访问权限 -
Tracer
: 负责创建,获取span的类 -
span
: 用来跟踪操作的API
API中的一些相关内容
-
TracerProvider
类型TracerProvider主要用来对
Tracer
进行相关访问,如创建tracer或获取一个已有tracer -
Tracer
类型Tracer主要用来创建或获取
span
-
Context交互(后面解释context)
能够从
context
实例中提取span
能够往
context
实例中注入span
-
span接口操作
Get Context
: 从指定span中得到对应的spanContextSet Attributes
: 给span设置一些属性,如键值对形式的元数据,这样可以方便后续查询,过滤和分析trace数据Attributes是作为元数据应用于span的键值对,利用这些键值对属性能够方便聚集,筛选和分组trace。Attributes可以在创建span时就添加,也可以在span的结束前的任意时刻添加。
Add Event
: 添加一个可读的消息Event是一个可读的信息,它描述了一个span在整个生命周期“正在发生的事情”
2.2. Metrics
下面的概念基本都是用来对trace和span进行进一步的描述修饰。
(1) metric
metric
是有关计算机程序运行时的执行情况的一个被捕获的measurement
,这个measurement与这个服务有关。metric的示例可以是“统计完成的请求数”、“统计活动请求数”、“获取队列长度”或“获取缓存未命中数”。
(2) measurement
measurement
代表一个通过metric API向SDK报告的数据(data point)。
一个measurement必须要封装:
- 一个值或变量(被观测的值)
- 一些属性(用来查询等)
(3) instrument
Instrument
s用于汇报measurement
,每个instrument必须要有以下信息:
- Instrument的名称
- instrument的类型
- 可选的度量单位
- 可选的描述
instruments在创建时就与meter
相关联,并通过名称标识
metric instruments的类型
-
Counter
counter
计数器支持Add()函数,该函数只接受正值。用来对某些数据进行计数。如 字节接受数,请求完成数,错误发生率等。counter适合用来计算比率,如请求率,错误率等。 -
UpDownCounter
UpDownCounter
是counter
的补充,它一样支持Add()函数,但该函数可以接受正值和负值。它比较实合用来监视一个请求中某些数额的增减(如系统资源),如: 当前出于活跃状态的请求的数目, 内存的使用量, 队列的长度等。 -
ValueRecorder
ValueRecorder
一般用来从记录分布或摘要中离散事件的值,并通过其Record()方法来捕获所有同步比率,平均值和范围所需的所有信息。比如延迟就是ValueRecorder最典型的例子。 -
SumObserver
对于异步的测量,如果它都是基于每个请求的单调总和那将是不必要的。这时
SumObserver
中的Observe()就排上用场。它的使用场景: 缓存未命中,系统CPU -
UpDownSumObserver
SumObserver的补充,其Observe()可以定时接受正总和或负总和。应用场景: 出于活跃状态的碎片数目, 进程堆大小
-
ValueObserver
可以用更细粒度地控制何时进行非加性地测量。
(4) Metrics API
metrics API主要由以下组成:
MeterProvider
: API的入口点,它提供了Meters的访问权限
Meter
:负责创建,获取Instruments的类
Instrument
: 负责报告Measurements
2.3. Logs
log
是带有时间戳的文本记录,可以是结构化也可以是非结构化,并带有元数据。log虽然是独立的数据源但它可以加到span中。在Opentelemetry中,任何不属于分布式trace或span的数据都是日志。例如,event
s就是特殊类型的日志。日志通常用于确定问题的根本原因,其通常包含的信息有谁更改了什么内容以及此次更改的结果。
3. 例子(Go)
虽然上述的概念很多很复杂,但实际应用中已经有很多包装好的第三方库可以直接使用,如otelhttp,otelsql,redisotel等等,以otelhttp为例看其代码。
(1) 新建Transport时配置span,初始化相关变量
func NewTransport(base http.RoundTripper, opts ...Option) *Transport {
if base == nil {
base = http.DefaultTransport
}
t := Transport{
rt: base,
}
// span配置
defaultOpts := []Option{
WithSpanOptions(trace.WithSpanKind(trace.SpanKindClient)),
WithSpanNameFormatter(defaultTransportFormatter),
}
c := newConfig(append(defaultOpts, opts...)...)
t.applyConfig(c)
return &t
}
func newConfig(opts ...Option) *config {
// 初始化
c := &config{
Propagators: otel.GetTextMapPropagator(),
TracerProvider: otel.GetTracerProvider(),
MeterProvider: global.GetMeterProvider(),
}
// 应用配置,如span类型等
for _, opt := range opts {
opt.Apply(c)
}
// 得到tracer和meter
c.Tracer = c.TracerProvider.Tracer(
instrumentationName,
trace.WithInstrumentationVersion(contrib.SemVersion()),
)
c.Meter = c.MeterProvider.Meter(
instrumentationName,
metric.WithInstrumentationVersion(contrib.SemVersion()),
)
return c
}
(2) 发送请求时创建一个span,并在结束时end这个span
func (t *Transport) RoundTrip(r *http.Request) (*http.Response, error) {
...
opts := append([]trace.SpanOption{}, t.spanStartOptions...) // start with the configured options
ctx, span := t.tracer.Start(r.Context(), t.spanNameFormatter("", r), opts...)
r = r.WithContext(ctx)
// span设置键值对属性
span.SetAttributes(semconv.HTTPClientAttributesFromHTTPRequest(r)...)
// 使用Propagator将其inject到http的请求头中
// 这里使用HeaderCarrier让header满足carrier,注意这里是request的header
t.propagators.Inject(ctx, propagation.HeaderCarrier(r.Header))
res, err := t.rt.RoundTrip(r)
if err != nil {
span.RecordError(err)
span.End()
return res, err
}
// 上文提到span的Attributes可在结束前任意时刻添加,这里再其请求返回后添加
span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(res.StatusCode)...)
span.SetStatus(semconv.SpanStatusFromHTTPStatusCode(res.StatusCode))
res.Body = &wrappedBody{ctx: ctx, span: span, body: res.Body}
return res, err
}
func (wb *wrappedBody) Read(b []byte) (int, error) {
n, err := wb.body.Read(b)
// 读完返回的数据后即end它的span
switch err {
case nil:
// nothing to do here but fall through to the return
case io.EOF:
wb.span.End()
default:
wb.span.RecordError(err)
}
return n, err
}
(3) 新建handler时添加metric instruments
type Handler struct {
operation string
handler http.Handler
tracer trace.Tracer
meter metric.Meter
propagators propagation.TextMapPropagator
spanStartOptions []trace.SpanOption
readEvent bool
writeEvent bool
filters []Filter
spanNameFormatter func(string, *http.Request) string
counters map[string]metric.Int64Counter //metric instruments
valueRecorders map[string]metric.Int64ValueRecorder //metric instruments
}
func NewHandler(handler http.Handler, operation string, opts ...Option) http.Handler {
h := Handler{
handler: handler,
operation: operation,
}
defaultOpts := []Option{
WithSpanOptions(trace.WithSpanKind(trace.SpanKindServer)),
WithSpanNameFormatter(defaultHandlerFormatter),
}
c := newConfig(append(defaultOpts, opts...)...)
h.configure(c)
h.createMeasures() //创建metric instruments
return &h
}
func (h *Handler) createMeasures() {
h.counters = make(map[string]metric.Int64Counter)
h.valueRecorders = make(map[string]metric.Int64ValueRecorder)
// 分别创建metric instruments,并保存在struct中
requestBytesCounter, err := h.meter.NewInt64Counter(RequestContentLength)
handleErr(err)
responseBytesCounter, err := h.meter.NewInt64Counter(ResponseContentLength)
handleErr(err)
serverLatencyMeasure, err := h.meter.NewInt64ValueRecorder(ServerLatency)
handleErr(err)
h.counters[RequestContentLength] = requestBytesCounter
h.counters[ResponseContentLength] = responseBytesCounter
h.valueRecorders[ServerLatency] = serverLatencyMeasure
}
(4) 在响应request中利用instrument观测具体数据,并对response做相关处理
// ServeHTTP serves HTTP requests (http.Handler)
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
requestStartTime := time.Now()
for _, f := range h.filters {
if !f(r) {
// Simply pass through to the handler if a filter rejects the request
h.handler.ServeHTTP(w, r)
return
}
}
opts := append([]trace.SpanOption{
trace.WithAttributes(semconv.NetAttributesFromHTTPRequest("tcp", r)...),
trace.WithAttributes(semconv.EndUserAttributesFromHTTPRequest(r)...),
trace.WithAttributes(semconv.HTTPServerAttributesFromHTTPRequest(h.operation, "", r)...),
}, h.spanStartOptions...) // start with the configured options
// 从header中取出(extract)context
ctx := h.propagators.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
// 利用这个context创建一个新的子span
ctx, span := h.tracer.Start(ctx, h.spanNameFormatter(h.operation, r), opts...)
defer span.End()
// 这个函数在写响应时调用
readRecordFunc := func(int64) {}
if h.readEvent {
readRecordFunc = func(n int64) {
span.AddEvent("read", trace.WithAttributes(ReadBytesKey.Int64(n)))
}
}
var bw bodyWrapper
// if request body is nil we don't want to mutate the body as it will affect
// the identity of it in a unforeseeable way because we assert ReadCloser
// fullfills a certain interface and it is indeed nil.
if r.Body != nil {
bw.ReadCloser = r.Body
bw.record = readRecordFunc
r.Body = &bw
}
writeRecordFunc := func(int64) {}
if h.writeEvent {
writeRecordFunc = func(n int64) {
span.AddEvent("write", trace.WithAttributes(WroteBytesKey.Int64(n)))
}
}
// 对response做包装,包装了例如在写响应头时自动将context注入(inject)等操作
rww := &respWriterWrapper{ResponseWriter: w, record: writeRecordFunc, ctx: ctx, props: h.propagators}
// Wrap w to use our ResponseWriter methods while also exposing
// other interfaces that w may implement (http.CloseNotifier,
// http.Flusher, http.Hijacker, http.Pusher, io.ReaderFrom).
w = httpsnoop.Wrap(w, httpsnoop.Hooks{
Header: func(httpsnoop.HeaderFunc) httpsnoop.HeaderFunc {
return rww.Header
},
Write: func(httpsnoop.WriteFunc) httpsnoop.WriteFunc {
return rww.Write
},
WriteHeader: func(httpsnoop.WriteHeaderFunc) httpsnoop.WriteHeaderFunc {
return rww.WriteHeader
},
})
labeler := &Labeler{}
ctx = injectLabeler(ctx, labeler)
h.handler.ServeHTTP(w, r.WithContext(ctx))
//这个函数在响应http后给span添加相关的属性,如读的字节数等
setAfterServeAttributes(span, bw.read, rww.written, rww.statusCode, bw.err, rww.err)
// 添加metrics,并配置相关属性
attributes := append(labeler.Get(), semconv.HTTPServerMetricAttributesFromHTTPRequest(h.operation, r)...)
h.counters[RequestContentLength].Add(ctx, bw.read, attributes...) //读取请求的字节数
h.counters[ResponseContentLength].Add(ctx, rww.written, attributes...) //写进响应的字节数
elapsedTime := time.Since(requestStartTime).Microseconds() //记录整个过程的延迟时间
h.valueRecorders[ServerLatency].Record(ctx, elapsedTime, attributes...)
}
其他实例可以见具体源代码或opentelemetry官方的示例
自己的例子
模块1的代码,这里是gin给前端的路由接口
func Login(c *gin.Context) {
ctx, span := misc.Tracer.Start(c.Request.Context(), "login") //全局tracer start span
defer span.End()
var formLogin FormLogin
var err error
if err = c.ShouldBindJSON(&formLogin); err != nil {
misc.Logger.Error("handler bind json err", zap.String("err", err.Error()))
misc.FailWithMsg(c, err.Error())
return
}
req := &proto.LoginRequest{
BaseRequest: proto.BaseRequest{
Meta: make(map[string]string),
},
Name: formLogin.UserName,
Password: misc.Sha1(formLogin.Password),
}
span.SetAttributes(attribute.String("userName", req.Name)) //span 打标签
code, authToken, errMsg := rpc.Login(ctx, req)
// 省略
}
//Login api rpc调用login
func Login(ctx context.Context, req *proto.LoginRequest) (code int, authToken string, errMsg string) {
otel.GetTextMapPropagator().Inject(ctx, req) //span注入,这里req实现了TextMapCarrier接口,详见下
reply := &proto.LoginResponse{}
if err := LogicRpcClient.Call(ctx, "Login", req, reply); err != nil {
errMsg = err.Error()
}
code = reply.Code
authToken = reply.AuthToken
return
}
模块2代码
//实现TextMapCarrier接口
type BaseRequest struct {
Meta map[string]string
}
func (l *BaseRequest) Get(key string) string {
return l.Meta[key]
}
func (l *BaseRequest) Set(key string, value string) {
l.Meta[key] = value
}
func (l *BaseRequest) Keys() []string {
var keys []string
for key := range l.Meta {
keys = append(keys, key)
}
return keys
}
//LoginRequest 登录请求
type LoginRequest struct {
BaseRequest
Name string
Password string
}
//Login 登录
func (rpc *RpcLogic) Login(rCtx context.Context, req *proto.LoginRequest, reply *proto.LoginResponse) error {
pCtx := otel.GetTextMapPropagator().Extract(rCtx, req)
ctx, span := misc.Tracer.Start(pCtx, "login-rpc")
defer span.End()
reply.Code = misc.FailReplyCode
// 业务处理
reply.Code = misc.SuccessReplyCode
reply.AuthToken = randToken
return nil
}
4. 参考
https://opentelemetry.io/
https://opentelemetry.lightstep.com/
https://github.com/open-telemetry/opentelemetry-specification
https://thanhnamit.medium.com/applying-observability-with-opentelemetry-part-2-metrics-and-logs-3d4913302ad2
https://www.cnblogs.com/charlieroro/p/13883578.html
https://lightstep.com/blog/opentelemetry-context-propagation/