% @# U) J/ K, d 原标题:基于 Grafana LGTM 可观测平台的构建 ) H6 L4 @2 [0 x7 c( F6 _
) v6 o, @9 S2 a5 x( I 可观测性目前属于云原生一个比较火的话题,它涉及的内容较多,不仅涉及多种遥测数据(信号),例如日志(log)、指标(metric)、分布式追踪(trace)、连续分析(continuous profiling)、 事件(event);还涉及遥测数据各生命周期管理,比如暴露、采集、存储、计算查询、统一看板。
( y9 V3 q2 K4 E 目前社区相关开源产品较多,各有各的优势,今天我们就来看看如何使用 Grafana LGTM 技术栈(Grafana、Loki、Tempo、Mimir)快速构建一个自己的可观测性平台。 3 j3 p, n% j; U2 v% V; [
通过本文你将了解:
2 G7 \( q1 s* _; W( \2 N) ^ 如何在 Go 程序中导出 metric、trace、log、以及它们之间的关联 TraceID
4 n; N4 n, s- V" @+ R& M [8 M 如何使用 OTel Collector 进行 metric、trace 收集 * |5 Z* X! H0 q4 r# Y9 }( s* @
如何使用 OTel Collector Contrib 进行日志收集 9 j, Z3 q$ }' z# m
如何部署 Grafana Mimir、Loki、Tempo 进行 metric、trace、log 数据存储 % l& e$ M5 L- O; _7 K/ F4 V, h
如何使用 Grafana 制作统一可观测性大盘 * m0 z# i4 x; T! c3 \5 Y7 I3 z
为了本次的教学内容,我们提前编写了一个 Go Web 程序,它提供 /v1/books 和 /v1/books/1 两个 HTTP 接口。 + B& I$ `% H& T8 G( U
当请求接口时,会先访问 Redis 缓存,如果未命中将继续访问 MySQL;整个请求会详细记录相关日志、整个链路各阶段调用情况以及整体请求延迟,当请求延迟 >200ms 时,会通过 Prometheus examplar 记录本次请求的 TraceID,用于该请求的日志、调用链关联。
) f' U. N6 u! |: U, U 下载并体验样例
0 @$ Q& f2 |, i- G; t2 s1 w 我们已经提前将样例程序上传到 github,所以您可以使用 git 进行下载:
9 f" [5 t' q7 } git clone https://github.com/grafanafans/prometheus-exemplar.git # ~1 j/ Z( U; g( o: n( q
cd prometheus-exemplar
$ Y3 `+ m* @$ T3 U3 C* } 使用 docker-compose 启动样例程序: 9 H( l% _/ ^3 _; c$ x- R
docker-compose up -d & Q( K$ W3 D/ P2 Y
这个命令会启动以下程序: 0 V. _+ u4 ~( Q' p; d, S
使用单节点模式分别启动一个 Mimir、Loki、Tempo 4 b/ n' J1 K" K" W, i
启动一个 Nginx 作为统一可观测平台查询入口,后端对接 Mimir、Loki、Tempo
& S5 m: N; p* P D5 e9 _ 启动 demo app, 并启动其依赖的 MySQL 和 Redis, demo app 可以使用 http://localhost:8080 访问 2 g: U! \% G7 {; d* }! L: @
启动 Grafana 并导入预设的数据源和 demo app 统一看板,可以使用 http://localhost:3000 访问
$ ?9 y3 `5 u8 N4 [; d3 K& D3 O 整个部署架构如下: # e: w" a" U; ], ~7 o

$ v* R! r6 p+ Z 当程序部署完成后,我们可以使用 wrk 进行 demo app 接口批量请求: & J8 W) a# N) k6 F) `. z6 m
wrk http://localhost:8080/v1/books : \& b( i* M% s5 |6 a2 l; G
wrk http://localhost:8080/v1/books/1 . q4 k& s; t: u/ D1 l
最后通过 http://localhost:3000 页面访问对应的看板:
2 ]/ \, F' P0 T  & ~' C' n0 Q& C2 S% m- G8 v& U
细节说明
5 G7 V* i; F9 j2 u 使用 Promethues Go SDK 导出 metrics ) v4 t4 `8 z V( h/ |
在 demo app 中,我们使用 Prometheus Go SDK 作为 metrics 导出,这里没有使用 OpenTelmetry SDK 主要因为当前版本(v0.33.0)还不支持 exemplar, 代码逻辑大致为: a0 {8 n+ g f6 D$ J" s
func Metrics(metricPath string, urlMapping func(string) string) gin.HandlerFunc <{p> httpDurationsHistogram := prometheus.NewHistogramVec(prometheus.HistogramOpts<{p> Name: "http_durations_histogram_seconds", # P. {7 n, d9 }
Help: "Http latency distributions.", * d# e! g2 ?8 ^# u4 c G
Buckets: []float64{0.05, 0.1, 0.25, 0.5, 1, 2}, % W; T% L4 U. v. d, D8 ^
}, []string{"method", "path", "code"})
6 e) d% |; T" V+ a# ^% t6 N prometheus.MustRegister(httpDurationsHistogram)
! x6 p% y& @7 r7 j k return func(c *gin.Context) <{p> .....
! Y; ]8 ]( ~. b+ a; y d0 D observer := httpDurationsHistogram.WithLabelValues(method, url, status) 1 ?2 t$ e0 P; r) f/ v# L/ i
observer.Observe(elapsed)
: R! c C* S# O0 Y* ] if elapsed > 0.2 <{p> observer.(prometheus.ExemplarObserver).ObserveWithExemplar(elapsed, prometheus.Labels<{p> "traceID": c.GetHeader(api.XRequestID), 1 x! P' Y$ m+ A' s0 ?+ b! Q3 E: v
}) ; M5 G! y1 I, X2 O2 L
} / x9 ^* d; i8 {) [6 ^2 x& ~
} : Y/ h* o: i# }# Z8 n+ O m4 e6 ^
} 4 R$ m- k& i% x, Q3 V
使用 OTLP HTTP 导出 traces 1 ^. |9 b# m+ P
使用 OTel SDK 进行 trace 埋点:
- n- m) ]* ~: C) O func (*MysqlBookService) Show(id string, ctx context.Context) (item *Book, err error) <{p> _, span := otel.Tracer().Start(ctx, "MysqlBookService.Show")
; |0 Y' L% K4 F span.SetAttributes(attribute.String("id", id))
6 r4 S' W" z. m+ n3 }* C defer span.End() 2 _. g) t' {$ ]7 ~
// mysql qury random time duration
0 h U; w( j5 L8 P time.Sleep(time.Duration(rand.Intn(250)) * time.Millisecond) 4 I# S' v' D+ v
err = db.Where(Book{Id: id}).Find(&item).Error & J, H+ T' c+ b: @ h$ k* v
return 3 r3 S( Z, n7 c* }
}
) I* W4 a- a/ }6 G 使用 OLTP HTTP 进行导出: . E: r0 s3 M3 ~9 ~' }$ f5 ^$ }
func SetTracerProvider(name, environment, endpoint string) error <{p> serviceName = name ! U- r4 x( K6 d3 [. i2 y3 y, @
client := otlptracehttp.NewClient(
* x/ W0 A' g6 K% Z. g8 a! f otlptracehttp.WithEndpoint(endpoint), 0 j& S( S; x9 }( a% I
otlptracehttp.WithInsecure(), & @4 M# R! o+ m5 m
)
! O! ^: p0 t2 u- L% H9 P2 N, [ exp, err := otlptrace.New(context.Background(), client)
3 z5 @- ^" t; C6 M, t$ E; X/ a if err != nil <{p> return err
: D! }- u @5 {8 {" h }
! O$ e, d, @$ v1 a0 C tp := tracesdk.NewTracerProvider(
. H# _5 v) F( \' [' V: L6 K tracesdk.WithBatcher(exp), 1 H8 h- H+ |( {6 }# m0 K! R2 z
tracesdk.WithResource(resource.NewWithAttributes( " a& I% [+ A3 I( p X' j
semconv.SchemaURL, / Q* y& ~2 `6 Z! b6 |
semconv.ServiceNameKey.String(serviceName),
~- c) f1 U0 q1 q7 H9 I/ z2 t attribute.String("environment", environment),
/ s) r& v1 n; q1 ~! p )), * X+ P B Q) g- Q4 [
) 0 C, s/ j2 X) N! Z, n
otel.SetTracerProvider(tp)
, K" G- s: z) M* ?# Y+ j return nil
/ K( H/ w; i+ _0 Q) L. _# Y }
5 N! H& W% p8 o5 m6 ~- ^7 M 结构化日志
1 A) M; |4 \! j/ A, ` 这里我们使用 go.uber.org/zap 包进行结构化日志输出,并输出到 /var/log/app.log 文件,每个请求开始时,注入 traceID:
H2 m( l7 D1 A5 ~" s cfg := zap.NewProductionConfig() / b, U% |4 ]- {9 q& u* {& s1 ?
cfg.OutputPaths = []string{"stderr", "/var/log/app.log"} $ `3 u2 ]9 I" n- m$ H* r/ `
logger, _ := cfg.Build()
% U8 J4 ]6 }5 `& ^4 G/ [ logger.With(zap.String("traceID", ctx.GetHeader(XRequestID))) * h9 Y" W, r' u A6 q
使用 OTel Collector 进行 metric、trace 收集 . j7 ~' V8 N. X" i
因为 demo app 的 metrics 使用 Prometheus SDK 导出,所以 OTel Collector 需要使用 Prometheus recevier 进行抓取,然后我们再通过 Prometheus remotewrite 将数据 push 到 Mimir。 " e. r6 Z4 Y: c$ R$ T9 z% ~) P
针对 traces,demo app 使用 OTLP HTTP 进行了导出,所有 Collector 需要用 OTP HTTP recevier 进行接收,最后再使用 OTLP gRPC 将数据 push 到 Tempo,对应配置如下: 2 k- O w4 c+ i. }; D; `
receivers:
. l5 V; C' Z. r# N otlp:
9 m$ D+ \7 c3 _2 z j. ^: u t protocols: . `0 |; ?; b9 L+ {2 \% }. V6 h
grpc:
0 p! O: u$ v. {4 Q$ Y http:
. l) t" n6 { w2 z) u% R1 _1 { prometheus:
7 I, o" k+ c$ W( ?' e9 P2 x& l config:
8 v: V6 K2 \0 F5 M! A scrape_configs: + |& L1 `3 d9 ^3 l- ^
- job_name: app
! _) V7 A' ~2 u( V0 {8 ?8 Z% \ scrape_interval: 10s
* d$ Q" q! ^, ~1 b3 T+ U& b static_configs: 6 q" W9 |/ t1 S( ?$ s
- targets: [app:8080] - p" O0 [# N8 x' O5 F z( `
exporters:
0 r& R6 d: l$ P# E5 z- x' k! B! W otlp: 6 p# ]" x8 I" r5 @; L$ G0 Z8 N( T
endpoint: tempo:4317
) E% }) F! }/ w3 f& i3 g) I tls:
5 u2 r9 q2 V9 \5 Z insecure: true
* G: h% W- G' F- U; ^" y! Y prometheusremotewrite: / e, q9 [# [5 k% b! }( k
endpoint: http://mimir:8080/api/v1/push / v9 P5 r4 E9 `( ]) U$ w
tls: 2 |! z* Z& j" Z3 c) R$ u2 }9 b
insecure: true
O. ~5 P; z, V( `, \ headers:
5 j; W; E, u; |% ]$ P+ a X-Scope-OrgID: demo
{& h% ?% l8 H9 a& d( b) c processors: ! \/ {8 R1 d/ K. D4 ^" i
batch: # R2 |3 a, d9 F7 H1 i
service:
6 k! `/ g( v+ U) ~+ @ pipelines:
1 v8 F' W1 b8 Z0 X2 f' P6 s8 P traces:
* }( T/ j, U% D1 P receivers: [otlp] . A) x& _* D4 V6 @ J- {
processors: [batch]
8 B6 L J" M+ I' S8 G$ J exporters: [otlp] # v) x+ X7 S2 K* s
metrics:
0 }- j0 r/ S& f, H% p receivers: [prometheus] D/ S7 e( {$ p- s7 {7 O) A
processors: [batch] 7 m2 g6 }7 y3 H1 ?$ _2 \4 @
exporters: [prometheusremotewrite] # A, f% l K9 D' {$ ^
使用 OTel Collector Contrib 进行 log 收集
. ]$ O' z7 B8 O* V% `" y 因为我们结构化日志输出到了/var/log/app.log 文件,所以这里使用 filelog receiver 进行最新日志读取,最后再经过loki exporter 进行导出,配置如下:
& g' W/ P1 K+ N9 D6 A receivers: . k7 L& a& I+ ~# \8 W2 m6 t
filelog: 8 u; K Y( H% r4 r
include: [/var/log/app.log]
& V% V2 V9 o' j% Y. ]5 b exporters: ; H+ V% u- X* q
loki:
4 e+ I! o" |0 L: J% T endpoint: http://loki:3100/loki/api/v1/push 7 W: ]7 ]6 x7 G4 {
tenant_id: demo ) o4 D2 A. E' |6 u- \" u+ V% E
labels:
( z5 \" B$ m; S attributes:
7 q* J* c& A$ n, c% W. a. S log.file.name: "filename"
% g; G0 G1 j% u7 v) S processors:
7 @+ A: U% U& B% x. f2 _ batch:
& w! X6 A C* o. [2 q% r service: 0 ~7 d, E& _1 m9 o6 n4 c
pipelines: 4 ~3 x; P' [4 ^" N" Z$ M7 E$ f* L
logs: 4 m* N9 m* `2 I$ V. k9 O( }
receivers: [filelog]
6 g+ K8 }5 F0 T- [) u R6 Q0 V0 m processors: [batch] ! q' q0 o; |% |0 l1 t5 H
exporters: [loki]
1 t: F. U4 O0 A* L/ D 以上就是有关 demo app 可观测性与 Grafana LGTM 技术栈集成的核心代码与配置,全部配置请参考 https://github.com/grafanafans/prometheus-exemplar 。 6 |5 O' M! `/ {' ^4 E9 F& j
总结 2 `! `. V p' G: T& i
本文我们通过一个简单的 Go 程序,导出了可观测性相关的遥测数据,其中包括 metrics、traces、logs, 然后统一由 OTel Collector 进行抓取,分别将三种遥测数据推送到 Grafana 的 Mimir、 Tempo、Loki 进行存储,最后再通过 Grafana 统一看板并进行 metrics、traces、logs 关联查询。 / ? \3 w' _4 O0 R" h8 _1 d
这里关联的逻辑为使用 Prometheus 的 exemplar 记录采样对应的 traceID,然后通过该 traceID 进行相关日志和 trace 查询。返回搜狐,查看更多 - }8 l6 q1 ]% t+ ^7 L. I( u
4 T0 O9 B( ~! i P( s# U7 B 责任编辑: 5 O1 R; y2 ~, F" D8 G
$ m; v/ H% @0 Q
$ @& I: O, W \- Q1 z2 b4 A# S; G/ s2 {+ s3 {
# q/ A. P. V* m5 o
|