Opentelemetry 介绍
OpenTelemetry 提供了一套 API、库、代理和采集器,帮助开发者收集、处理和导出分布式系统的可观测性数据。
腾讯云可观测APM服务,就接受OTLP协议的数据上报,可以实现链路跟踪,应用性能,接口耗时等颗粒度很细的可观测数据。
Opentelemetry SDK 初始化
func setupOTelSDK(ctx context.Context) (shutdown func(context.Context) error, err error) {
var shutdownFuncs []func(context.Context) error
shutdown = func(ctx context.Context) error {
var err error
for _, fn := range shutdownFuncs {
err = errors.Join(err, fn(ctx))
}
shutdownFuncs = nil
return err
}
handleErr := func(inErr error) {
err = errors.Join(inErr, shutdown(ctx))
}
tracerProvider, err := newTraceProvider(ctx)
if err != nil {
handleErr(err)
return
}
shutdownFuncs = append(shutdownFuncs, tracerProvider.Shutdown)
otel.SetTracerProvider(tracerProvider)
meterProvider, err := newMeterProvider()
if err != nil {
handleErr(err)
return
}
shutdownFuncs = append(shutdownFuncs, meterProvider.Shutdown)
otel.SetMeterProvider(meterProvider)
return
}
func newTraceProvider(ctx context.Context) (*trace.TracerProvider, error) {
opts := []otlptracegrpc.Option{
otlptracegrpc.WithEndpoint("<endpoint>"), // <endpoint>替换为上报地址
otlptracegrpc.WithInsecure(),
}
exporter, err := otlptracegrpc.New(ctx, opts...)
if err != nil {
log.Fatal(err)
}
r, err := resource.New(ctx, []resource.Option{
resource.WithAttributes(
attribute.KeyValue{Key: "token", Value: attribute.StringValue("<token>")}, // <token>替换为业务系统Token
attribute.KeyValue{Key: "service.name", Value: attribute.StringValue("<serviceName>")}, // <serviceName>替换为应用名
attribute.KeyValue{Key: "host.name", Value: attribute.StringValue("<hostName>")}, // <hostName>替换为IP地址
),
}...)
if err != nil {
log.Fatal(err)
}
tp := sdktrace.NewTracerProvider(
sdktrace.WithSampler(sdktrace.AlwaysSample()),
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(r),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
return tp, nil
}
func newMeterProvider() (*metric.MeterProvider, error) {
metricExporter, err := stdoutmetric.New()
if err != nil {
return nil, err
}
meterProvider := metric.NewMeterProvider(
metric.WithReader(metric.NewPeriodicReader(metricExporter,
// Default is 1m. Set to 3s for demonstrative purposes.
metric.WithInterval(3*time.Second))),
)
return meterProvider, nil
}
http Server 接入
实现 Handler中间件
- 从request header 中获取trace信息,和request context 合并成最新的一个context
ctx
。 - 使用最新的context
ctx
启动 trace,获得一个带trace信息的contextspanCtx
。 - 将
spanCtx
重新注入到下一个request header中,串联起整个链路。
func TraceHandler(serviceName, path string, opts ...TraceOption) func(http.Handler) http.Handler {
var options traceOptions
for _, opt := range opts {
opt(&options)
}
ignorePaths := collection.NewSet()
ignorePaths.AddStr(options.traceIgnorePaths...)
return func(next http.Handler) http.Handler {
tracer := otel.Tracer(TraceName)
propagator := otel.GetTextMapPropagator()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
spanName = path
if len(spanName) == 0 {
spanName = r.URL.Path
}
if ignorePaths.Contains(spanName) {
next.ServeHTTP(w, r)
return
}
ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
spanCtx, span := tracer.Start(
ctx,
spanName,
oteltrace.WithSpanKind(oteltrace.SpanKindServer),
oteltrace.WithAttributes(semconv.HTTPServerAttributesFromHTTPRequest(
serviceName, spanName, r)...),
)
defer span.End()
propagator.Inject(spanCtx, propagation.HeaderCarrier(w.Header()))
next.ServeHTTP(w, r.WithContext(spanCtx))
span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(http.StatusOK)...)
span.SetStatus(semconv.SpanStatusFromHTTPStatusCodeAndSpanKind(
http.StatusOK, oteltrace.SpanKindServer))
})
}
}
http request 接入
将request context中的trace信息写入 propagator header 中传播。
func Request(r *http.Request) (*http.Response, error) {
ctx := r.Context()
tracer := TracerFromContext(ctx)
propagator := otel.GetTextMapPropagator()
spanName = r.URL.Path
ctx, span := tracer.Start(
ctx,
spanName,
oteltrace.WithSpanKind(oteltrace.SpanKindClient),
oteltrace.WithAttributes(semconv.HTTPClientAttributesFromHTTPRequest(r)...),
)
defer span.End()
r = r.WithContext(ctx)
propagator.Inject(ctx, propagation.HeaderCarrier(r.Header))
resp, err := http.DefaultClient.Do(r)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return resp, err
}
span.SetAttributes(semconv.HTTPAttributesFromHTTPStatusCode(resp.StatusCode)...)
span.SetStatus(semconv.SpanStatusFromHTTPStatusCodeAndSpanKind(resp.StatusCode, oteltrace.SpanKindClient))
return resp, err
}
Kong网关接入
腾讯云 kong网关目前暂不支持,已经提交安灯需求单(andon.woa.com/micro/base/…
服务接入
因为项目是使用go-zero 框架,只需要在项目中增加配置文件,服务启动时候配置对应的 opentelemetry endpoint和 token就行,也可以使用
- 服务配置文件开启opentelemetry endpoint
Telemetry:
Name: urp-api
Endpoint: pl.ap-shanghai.apm.tencentcs.com:4317
Batcher: otlpgrpc
- 服务deployment 文件配置 opentelemetry token, 以configmap 方式挂载到container的环境变量中即可。
apiVersion: v1
data:
OTEL_RESOURCE_ATTRIBUTES: token=xxxxx
kind: ConfigMap
metadata:
name: apm-resource-config
namespace: {{Namespace}}
MySQL 接入
trace定义:
var sqlAttributeKey = attribute.Key("sql.method")
func startSpan(ctx context.Context, method string) (context.Context, oteltrace.Span) {
tracer := trace.TracerFromContext(ctx)
start, span := tracer.Start(ctx, spanName, oteltrace.WithSpanKind(oteltrace.SpanKindClient))
span.SetAttributes(sqlAttributeKey.String(method))
return start, span
}
func endSpan(span oteltrace.Span, err error) {
defer span.End()
if err == nil || errors.Is(err, sql.ErrNoRows) {
span.SetStatus(codes.Ok, "")
return
}
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
}
trace接入:
ctx, span := startSpan(ctx, "Exec")
defer func() {
endSpan(span, err)
}()
// your work code here
MongoDB 接入
trace 定义:
var mongoCmdAttributeKey = attribute.Key("mongo.cmd")
func StartSpan(ctx context.Context, cmd string) (context.Context, oteltrace.Span) {
tracer := trace.TracerFromContext(ctx)
ctx, span := tracer.Start(ctx, spanName, oteltrace.WithSpanKind(oteltrace.SpanKindClient))
span.SetAttributes(mongoCmdAttributeKey.String(cmd))
return ctx, span
}
func EndSpan(span oteltrace.Span, err error) {
defer span.End()
if err == nil || errorsx.In(err, mongo.ErrNoDocuments, mongo.ErrNilValue, mongo.ErrNilDocument) {
span.SetStatus(codes.Ok, "")
return
}
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
}
trace 使用:
ctx, span := trace.StartSpan(p.ctx, "GetProductOneSpec")
defer func() {
trace.EndSpan(span, err)
}()
// your work code
Redis 接入
trace定义:
var redisCmdsAttributeKey = attribute.Key("redis.cmds")
func startSpan(ctx context.Context, cmdStrs string) (context.Context, oteltrace.Span) {
tracer := trace.TracerFromContext(ctx)
start, span := tracer.Start(ctx, spanName, oteltrace.WithSpanKind(oteltrace.SpanKindClient))
span.SetAttributes(redisCmdsAttributeKey.StringSlice(cmdStrs))
return start, span
}
func endSpan(span oteltrace.Span, err error) {
defer span.End()
if err == nil || errors.Is(err, red.Nil) {
span.SetStatus(codes.Ok, "")
return
}
span.SetStatus(codes.Error, err.Error())
span.RecordError(err)
}
trace接入:
ctx, span := startSpan(ctx, "set")
defer func() {
endSpan(span, err)
}()
// your work code here