Go遥测可观测性实战:用结构化日志和指标构建生产级可观测性的5个核心模式
2026年,Go语言的遥测生态已经从"可选"变成了"必选"。随着微服务架构的深度普及,一个线上问题如果无法在5分钟内定位,就意味着用户流失和收入损失。Go 1.22引入的slog标准库、OpenTelemetry Go SDK的成熟、以及各大云厂商对OTLP协议的全面支持,让Go服务的可观测性终于有了统一的答案。但现实是:很多团队仍然在用fmt.Println调试线上问题,日志和指标割裂,追踪链路断裂,告警风暴不断。本文将从5个核心模式出发,带你构建真正能用的生产级可观测性体系。
核心概念
| 概念 | 说明 | 关键包/工具 |
|---|---|---|
| slog结构化日志 | Go 1.22+标准库,支持键值对结构化输出 | log/slog |
| OpenTelemetry Metrics | 统一指标采集标准,支持Counter/Gauge/Histogram | go.opentelemetry.io/otel/metric |
| 分布式追踪 | 跨服务请求链路追踪,W3C TraceContext传播 | go.opentelemetry.io/otel/trace |
| Instrumentation中间件 | 自动化的HTTP/gRPC拦截埋点 | go.opentelemetry.io/contrib/instrumentation |
| 可观测性仪表盘 | Grafana/Prometheus集成的统一监控视图 | Grafana, Prometheus, Loki |
问题分析:Go可观测性的5大痛点
痛点1:日志无结构,排查如大海捞针
传统log.Printf输出纯文本,无法被机器解析,日志平台无法建立索引,排查问题时只能靠肉眼搜索。
痛点2:指标与日志割裂,无法关联分析
指标显示CPU飙升,但无法直接跳转到对应时间段的日志,两个系统各自为政,问题定位效率极低。
痛点3:分布式追踪链路断裂
服务A调用服务B,但追踪ID没有正确传播,导致链路在边界处断裂,无法看到完整的请求路径。
痛点4:手动埋点代码侵入严重
每个HTTP handler都要手动写一堆埋点代码,业务逻辑被可观测性代码淹没,维护成本极高。
痛点5:生产环境告警风暴
缺乏合理的指标聚合和告警策略,一个服务抖动触发几十条告警,反而掩盖了真正的问题。
核心模式1:slog结构化日志与上下文传递
slog是Go 1.22引入的结构化日志标准库,它不仅支持键值对输出,更重要的是支持Context传递,让日志自动携带请求级别的上下文信息。
package main
import (
"context"
"log/slog"
"os"
"time"
)
// RequestIDKey 用于从context中提取请求ID的键
type RequestIDKey struct{}
// Logger 封装slog.Logger,提供上下文感知的日志方法
type Logger struct {
inner *slog.Logger
}
// NewLogger 创建带有默认字段的Logger
func NewLogger(serviceName string) *Logger {
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelInfo,
})
logger := slog.New(handler).With(
"service", serviceName,
"pid", os.Getpid(),
)
return &Logger{inner: logger}
}
// WithContext 从context中提取请求信息并附加到日志
func (l *Logger) WithContext(ctx context.Context) *slog.Logger {
logger := l.inner
if reqID, ok := ctx.Value(RequestIDKey{}).(string); ok {
logger = logger.With("request_id", reqID)
}
if traceID := getTraceIDFromContext(ctx); traceID != "" {
logger = logger.With("trace_id", traceID)
}
return logger
}
// InfoContext 记录Info级别日志,自动携带上下文
func (l *Logger) InfoContext(ctx context.Context, msg string, args ...any) {
l.WithContext(ctx).InfoContext(ctx, msg, args...)
}
// ErrorContext 记录Error级别日志,自动携带上下文
func (l *Logger) ErrorContext(ctx context.Context, msg string, args ...any) {
l.WithContext(ctx).ErrorContext(ctx, msg, args...)
}
// getTraceIDFromContext 从OpenTelemetry context中提取traceID
func getTraceIDFromContext(ctx context.Context) string {
span := trace.SpanFromContext(ctx)
if span.SpanContext().IsValid() {
return span.SpanContext().TraceID().String()
}
return ""
}
// --- 使用示例 ---
// middlewareRequestID HTTP中间件:注入请求ID到context
func middlewareRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
reqID := r.Header.Get("X-Request-ID")
if reqID == "" {
reqID = generateUUID()
}
ctx := context.WithValue(r.Context(), RequestIDKey{}, reqID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// handleGetUser 业务handler:使用上下文感知日志
func handleGetUser(logger *Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
userID := r.PathValue("id")
logger.InfoContext(ctx, "fetching user",
"user_id", userID,
"method", r.Method,
"path", r.URL.Path,
)
user, err := fetchUserFromDB(ctx, userID)
if err != nil {
logger.ErrorContext(ctx, "failed to fetch user",
"user_id", userID,
"error", err.Error(),
"duration_ms", time.Since(time.Now()).Milliseconds(),
)
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
logger.InfoContext(ctx, "user fetched successfully",
"user_id", userID,
"user_name", user.Name,
)
json.NewEncoder(w).Encode(user)
}
}
关键要点:
- 使用
JSONHandler输出结构化JSON,便于日志平台解析和索引 - 通过
With()方法添加服务级默认字段,避免每条日志重复写 - 从
Context中提取请求ID和TraceID,实现日志与追踪的自动关联 WithContext模式让业务代码无需关心日志上下文传递细节
核心模式2:OpenTelemetry Metrics指标采集
OpenTelemetry Metrics提供了统一的指标采集标准,支持Counter、Gauge、Histogram三种指标类型,配合Prometheus导出器,可以无缝对接现有监控体系。
package main
import (
"context"
"fmt"
"net/http"
"time"
"go.opentelemetry.io/otel/exporters/prometheus"
"go.opentelemetry.io/otel/metric"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
)
// MetricsProvider 封装OpenTelemetry MeterProvider
type MetricsProvider struct {
provider *sdkmetric.MeterProvider
meter metric.Meter
}
// NewMetricsProvider 创建MetricsProvider并注册Prometheus导出器
func NewMetricsProvider(serviceName string) (*MetricsProvider, error) {
exporter, err := prometheus.New()
if err != nil {
return nil, fmt.Errorf("create prometheus exporter: %w", err)
}
provider := sdkmetric.NewMeterProvider(
sdkmetric.WithReader(exporter),
sdkmetric.WithView(
sdkmetric.NewView(
sdkmetric.Instrument{Name: "http.server.duration"},
sdkmetric.Stream{
Aggregation: sdkmetric.AggregationExplicitBucketHistogram{
Boundaries: []float64{0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
},
},
),
),
)
meter := provider.Meter(
serviceName,
metric.WithInstrumentationVersion("1.0.0"),
)
return &MetricsProvider{
provider: provider,
meter: meter,
}, nil
}
// AppMetrics 应用级指标集合
type AppMetrics struct {
httpRequestsTotal metric.Int64Counter
httpRequestDuration metric.Float64Histogram
activeConnections metric.Int64UpDownCounter
dbQueryDuration metric.Float64Histogram
businessOpsTotal metric.Int64Counter
}
// NewAppMetrics 初始化所有应用指标
func NewAppMetrics(mp *MetricsProvider) (*AppMetrics, error) {
m := mp.meter
am := &AppMetrics{}
var err error
am.httpRequestsTotal, err = m.Int64Counter(
"http.server.requests.total",
metric.WithDescription("Total number of HTTP requests"),
metric.WithUnit("{request}"),
)
if err != nil {
return nil, err
}
am.httpRequestDuration, err = m.Float64Histogram(
"http.server.duration",
metric.WithDescription("HTTP request duration"),
metric.WithUnit("s"),
)
if err != nil {
return nil, err
}
am.activeConnections, err = m.Int64UpDownCounter(
"http.server.connections.active",
metric.WithDescription("Number of active connections"),
metric.WithUnit("{connection}"),
)
if err != nil {
return nil, err
}
am.dbQueryDuration, err = m.Float64Histogram(
"db.query.duration",
metric.WithDescription("Database query duration"),
metric.WithUnit("s"),
)
if err != nil {
return nil, err
}
am.businessOpsTotal, err = m.Int64Counter(
"business.operations.total",
metric.WithDescription("Total number of business operations"),
metric.WithUnit("{operation}"),
)
if err != nil {
return nil, err
}
return am, nil
}
// RecordHTTPRequest 记录HTTP请求指标
func (am *AppMetrics) RecordHTTPRequest(ctx context.Context, method, path, status string, duration time.Duration) {
attrs := metric.WithAttributes(
attribute.String("http.method", method),
attribute.String("http.route", path),
attribute.Int("http.status_code", statusCodeToInt(status)),
)
am.httpRequestsTotal.Add(ctx, 1, attrs)
am.httpRequestDuration.Record(ctx, duration.Seconds(), attrs)
}
// RecordDBQuery 记录数据库查询指标
func (am *AppMetrics) RecordDBQuery(ctx context.Context, query string, duration time.Duration, err error) {
attrs := metric.WithAttributes(
attribute.String("db.query.name", query),
attribute.Bool("db.query.error", err != nil),
)
am.dbQueryDuration.Record(ctx, duration.Seconds(), attrs)
}
// RecordBusinessOp 记录业务操作指标
func (am *AppMetrics) RecordBusinessOp(ctx context.Context, op string, success bool) {
attrs := metric.WithAttributes(
attribute.String("operation.type", op),
attribute.Bool("operation.success", success),
)
am.businessOpsTotal.Add(ctx, 1, attrs)
}
// --- 使用示例 ---
// metricsMiddleware HTTP中间件:自动采集请求指标
func metricsMiddleware(metrics *AppMetrics, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
metrics.activeConnections.Add(r.Context(), 1)
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(rw, r)
duration := time.Since(start)
metrics.RecordHTTPRequest(
r.Context(),
r.Method,
r.URL.Path,
fmt.Sprintf("%d", rw.statusCode),
duration,
)
metrics.activeConnections.Add(r.Context(), -1)
})
}
// responseWriter 包装http.ResponseWriter以捕获状态码
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
关键要点:
- 使用
ExplicitBucketHistogram自定义直方图桶边界,适配HTTP请求延迟分布 UpDownCounter适合追踪活跃连接数等可增可减的指标- 中间件自动采集指标,业务代码零侵入
- 通过
WithAttributes添加维度标签,支持多维度的指标聚合和筛选
核心模式3:分布式追踪与上下文传播
分布式追踪是可观测性的第三根支柱。通过W3C TraceContext标准,Go服务可以自动在HTTP/gRPC调用间传播追踪上下文,实现跨服务的请求链路追踪。
package main
import (
"context"
"fmt"
"net/http"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/propagation"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/trace"
)
// TracingProvider 封装OpenTelemetry TracerProvider
type TracingProvider struct {
provider *sdktrace.TracerProvider
tracer trace.Tracer
}
// NewTracingProvider 创建TracingProvider并配置OTLP导出
func NewTracingProvider(ctx context.Context, serviceName, otlpEndpoint string) (*TracingProvider, error) {
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(otlpEndpoint),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, fmt.Errorf("create OTLP exporter: %w", err)
}
provider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
semconv.ServiceVersionKey.String("1.0.0"),
)),
sdktrace.WithSampler(sdktrace.ParentBased(
sdktrace.TraceIDRatioBased(0.1), // 生产环境采样10%
)),
)
// 设置全局TracerProvider和传播器
otel.SetTracerProvider(provider)
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
tracer := provider.Tracer(
serviceName,
trace.WithInstrumentationVersion("1.0.0"),
)
return &TracingProvider{
provider: provider,
tracer: tracer,
}, nil
}
// Shutdown 优雅关闭TracerProvider
func (tp *TracingProvider) Shutdown(ctx context.Context) error {
return tp.provider.Shutdown(ctx)
}
// SpanBuilder 构建Span的流畅API
type SpanBuilder struct {
tracer trace.Tracer
name string
attrs []attribute.KeyValue
options []trace.SpanStartOption
}
// NewSpanBuilder 创建SpanBuilder
func (tp *TracingProvider) NewSpanBuilder(name string) *SpanBuilder {
return &SpanBuilder{
tracer: tp.tracer,
name: name,
}
}
// WithAttr 添加属性
func (sb *SpanBuilder) WithAttr(key string, value any) *SpanBuilder {
switch v := value.(type) {
case string:
sb.attrs = append(sb.attrs, attribute.String(key, v))
case int:
sb.attrs = append(sb.attrs, attribute.Int(key, v))
case bool:
sb.attrs = append(sb.attrs, attribute.Bool(key, v))
}
return sb
}
// WithOption 添加SpanStartOption
func (sb *SpanBuilder) WithOption(opt trace.SpanStartOption) *SpanBuilder {
sb.options = append(sb.options, opt)
return sb
}
// Do 执行带追踪的函数
func (sb *SpanBuilder) Do(ctx context.Context, fn func(ctx context.Context) error) error {
if len(sb.attrs) > 0 {
sb.options = append(sb.options, trace.WithAttributes(sb.attrs...))
}
ctx, span := sb.tracer.Start(ctx, sb.name, sb.options...)
defer span.End()
if err := fn(ctx); err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
span.SetStatus(codes.Ok, "")
return nil
}
// --- 使用示例 ---
// tracingMiddleware HTTP中间件:自动创建根Span
func tracingMiddleware(tp *TracingProvider, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
propagator := otel.GetTextMapPropagator()
ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header))
spanName := fmt.Sprintf("%s %s", r.Method, r.URL.Path)
ctx, span := tp.tracer.Start(ctx, spanName,
trace.WithAttributes(
semconv.HTTPRequestMethodKey.String(r.Method),
semconv.URLPathKey.String(r.URL.Path),
semconv.UserAgentOriginalKey.String(r.UserAgent()),
),
trace.WithSpanKind(trace.SpanKindServer),
)
defer span.End()
rw := &responseWriter{ResponseWriter: w, statusCode: http.StatusOK}
next.ServeHTTP(rw, r.WithContext(ctx))
span.SetAttributes(semconv.HTTPResponseStatusCodeKey.Int(rw.statusCode))
if rw.statusCode >= 400 {
span.SetStatus(codes.Error, fmt.Sprintf("HTTP %d", rw.statusCode))
}
})
}
// callUserService 调用用户服务的HTTP客户端,自动传播追踪上下文
func callUserService(ctx context.Context, tp *TracingProvider, userID string) (*User, error) {
var user User
err := tp.NewSpanBuilder("call-user-service").
WithAttr("user.id", userID).
WithOption(trace.WithSpanKind(trace.SpanKindClient)).
Do(ctx, func(ctx context.Context) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
fmt.Sprintf("http://user-service:8080/users/%s", userID), nil)
if err != nil {
return fmt.Errorf("create request: %w", err)
}
// 自动注入追踪上下文到HTTP头
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("do request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
return json.NewDecoder(resp.Body).Decode(&user)
})
return &user, err
}
关键要点:
- 使用
ParentBased采样策略,根请求采样10%,子请求跟随父决策,确保链路完整 TextMapPropagator自动在HTTP头中注入/提取TraceContext,实现跨服务传播SpanBuilder流畅API简化Span创建,减少样板代码- 客户端调用时必须调用
Inject,服务端中间件自动调用Extract
核心模式4:自定义Instrumentation中间件
OpenTelemetry Contrib提供了丰富的Instrumentation包,但生产环境往往需要自定义中间件来满足特定需求,如业务指标采集、敏感信息过滤、自定义Span属性等。
package main
import (
"context"
"fmt"
"net/http"
"time"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
)
// InstrumentationMiddleware 统一的可观测性中间件
type InstrumentationMiddleware struct {
logger *Logger
metrics *AppMetrics
tracer trace.Tracer
}
// NewInstrumentationMiddleware 创建InstrumentationMiddleware
func NewInstrumentationMiddleware(
logger *Logger,
metrics *AppMetrics,
tp *TracingProvider,
) *InstrumentationMiddleware {
return &InstrumentationMiddleware{
logger: logger,
metrics: metrics,
tracer: tp.tracer,
}
}
// InstrumentHTTP 完整的HTTP可观测性中间件
func (im *InstrumentationMiddleware) InstrumentHTTP(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
ctx := r.Context()
// 1. 追踪:创建服务端Span
propagator := otel.GetTextMapPropagator()
ctx = propagator.Extract(ctx, propagation.HeaderCarrier(r.Header))
spanName := fmt.Sprintf("HTTP %s %s", r.Method, r.URL.Path)
ctx, span := im.tracer.Start(ctx, spanName,
trace.WithSpanKind(trace.SpanKindServer),
trace.WithAttributes(
attribute.String("http.method", r.Method),
attribute.String("http.url", r.URL.String()),
attribute.String("http.user_agent", r.UserAgent()),
attribute.String("net.peer.ip", r.RemoteAddr),
),
)
defer span.End()
// 2. 指标:追踪活跃连接
im.metrics.activeConnections.Add(ctx, 1)
defer im.metrics.activeConnections.Add(ctx, -1)
// 3. 日志:记录请求开始
im.logger.InfoContext(ctx, "request started",
"method", r.Method,
"path", r.URL.Path,
"remote_addr", r.RemoteAddr,
)
// 4. 执行业务handler
rw := &instrumentedWriter{
ResponseWriter: w,
statusCode: http.StatusOK,
bytesWritten: 0,
}
next.ServeHTTP(rw, r.WithContext(ctx))
// 5. 记录响应指标
duration := time.Since(start)
im.metrics.RecordHTTPRequest(ctx, r.Method, r.URL.Path,
fmt.Sprintf("%d", rw.statusCode), duration)
// 6. 追踪:记录响应信息
span.SetAttributes(
attribute.Int("http.status_code", rw.statusCode),
attribute.Int("http.response_size", rw.bytesWritten),
attribute.Float64("http.duration_ms", float64(duration.Milliseconds())),
)
if rw.statusCode >= 500 {
span.SetStatus(codes.Error, fmt.Sprintf("HTTP %d", rw.statusCode))
}
// 7. 日志:记录请求完成
im.logger.InfoContext(ctx, "request completed",
"status_code", rw.statusCode,
"duration_ms", duration.Milliseconds(),
"bytes_written", rw.bytesWritten,
)
})
}
// instrumentedWriter 包装ResponseWriter以捕获响应信息
type instrumentedWriter struct {
http.ResponseWriter
statusCode int
bytesWritten int
}
func (iw *instrumentedWriter) WriteHeader(code int) {
iw.statusCode = code
iw.ResponseWriter.WriteHeader(code)
}
func (iw *instrumentedWriter) Write(b []byte) (int, error) {
n, err := iw.ResponseWriter.Write(b)
iw.bytesWritten += n
return n, err
}
// InstrumentDB 数据库查询可观测性装饰器
func (im *InstrumentationMiddleware) InstrumentDB(
ctx context.Context,
queryName string,
fn func(ctx context.Context) error,
) error {
start := time.Now()
ctx, span := im.tracer.Start(ctx, fmt.Sprintf("DB %s", queryName),
trace.WithSpanKind(trace.SpanKindClient),
trace.WithAttributes(
attribute.String("db.query.name", queryName),
),
)
defer span.End()
err := fn(ctx)
duration := time.Since(start)
im.metrics.RecordDBQuery(ctx, queryName, duration, err)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
im.logger.ErrorContext(ctx, "db query failed",
"query", queryName,
"error", err.Error(),
"duration_ms", duration.Milliseconds(),
)
} else {
im.logger.InfoContext(ctx, "db query completed",
"query", queryName,
"duration_ms", duration.Milliseconds(),
)
}
return err
}
// InstrumentGRPC gRPC可观测性拦截器(服务端)
func (im *InstrumentationMiddleware) InstrumentGRPC(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (interface{}, error) {
start := time.Now()
ctx, span := im.tracer.Start(ctx, info.FullMethod,
trace.WithSpanKind(trace.SpanKindServer),
)
defer span.End()
resp, err := handler(ctx, req)
duration := time.Since(start)
im.metrics.RecordHTTPRequest(ctx, "gRPC", info.FullMethod,
statusFromError(err), duration)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
im.logger.ErrorContext(ctx, "grpc call failed",
"method", info.FullMethod,
"error", err.Error(),
"duration_ms", duration.Milliseconds(),
)
} else {
im.logger.InfoContext(ctx, "grpc call completed",
"method", info.FullMethod,
"duration_ms", duration.Milliseconds(),
)
}
return resp, err
}
关键要点:
- 统一中间件同时处理日志、指标、追踪三大支柱,确保数据一致性
instrumentedWriter捕获响应状态码和字节数,丰富追踪和指标维度InstrumentDB装饰器为数据库查询添加可观测性,无需修改业务代码- gRPC拦截器与HTTP中间件共享同一套可观测性基础设施
核心模式5:生产级可观测性仪表盘集成
将日志、指标、追踪三大支柱统一集成到Grafana仪表盘,实现从告警到日志到追踪的一键跳转,是生产级可观测性的最终目标。
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"go.opentelemetry.io/otel/exporters/prometheus"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
)
// ObservabilityStack 生产级可观测性栈
type ObservabilityStack struct {
logger *Logger
metricsProvider *MetricsProvider
tracingProvider *TracingProvider
middleware *InstrumentationMiddleware
metrics *AppMetrics
shutdownFuncs []func(ctx context.Context) error
}
// Config 可观测性栈配置
type Config struct {
ServiceName string
OTLPEndpoint string
MetricsPath string
LogLevel string
SamplingRatio float64
}
// NewObservabilityStack 初始化完整的可观测性栈
func NewObservabilityStack(ctx context.Context, cfg Config) (*ObservabilityStack, error) {
stack := &ObservabilityStack{}
// 1. 初始化结构化日志
stack.logger = NewLogger(cfg.ServiceName)
// 2. 初始化指标采集
promExporter, err := prometheus.New()
if err != nil {
return nil, fmt.Errorf("create prometheus exporter: %w", err)
}
metricProvider := sdkmetric.NewMeterProvider(
sdkmetric.WithReader(promExporter),
)
stack.metricsProvider = &MetricsProvider{
provider: metricProvider,
meter: metricProvider.Meter(cfg.ServiceName),
}
stack.shutdownFuncs = append(stack.shutdownFuncs, metricProvider.Shutdown)
// 3. 初始化分布式追踪
otlpExporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint(cfg.OTLPEndpoint),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, fmt.Errorf("create OTLP exporter: %w", err)
}
traceProvider := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(otlpExporter),
sdktrace.WithSampler(sdktrace.ParentBased(
sdktrace.TraceIDRatioBased(cfg.SamplingRatio),
)),
)
stack.tracingProvider = &TracingProvider{
provider: traceProvider,
tracer: traceProvider.Tracer(cfg.ServiceName),
}
stack.shutdownFuncs = append(stack.shutdownFuncs, traceProvider.Shutdown)
// 4. 初始化应用指标
stack.metrics, err = NewAppMetrics(stack.metricsProvider)
if err != nil {
return nil, fmt.Errorf("create app metrics: %w", err)
}
// 5. 初始化统一中间件
stack.middleware = NewInstrumentationMiddleware(
stack.logger,
stack.metrics,
stack.tracingProvider,
)
return stack, nil
}
// Shutdown 优雅关闭所有可观测性组件
func (s *ObservabilityStack) Shutdown(ctx context.Context) error {
var firstErr error
for _, fn := range s.shutdownFuncs {
if err := fn(ctx); err != nil && firstErr == nil {
firstErr = err
}
}
return firstErr
}
// --- 完整启动示例 ---
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 初始化可观测性栈
stack, err := NewObservabilityStack(ctx, Config{
ServiceName: "user-service",
OTLPEndpoint: "otel-collector:4317",
MetricsPath: "/metrics",
LogLevel: "info",
SamplingRatio: 0.1,
})
if err != nil {
fmt.Fprintf(os.Stderr, "init observability: %v\n", err)
os.Exit(1)
}
defer stack.Shutdown(context.Background())
// 创建HTTP路由
mux := http.NewServeMux()
// 注册业务路由(带可观测性中间件)
mux.HandleFunc("GET /users/{id}", handleGetUser(stack.logger))
mux.HandleFunc("POST /users", handleCreateUser(stack.logger, stack.metrics))
// 注册Prometheus指标端点
mux.HandleFunc("/metrics", func(w http.ResponseWriter, r *http.Request) {
promHandler := prometheus.Handler()
promHandler.ServeHTTP(w, r)
})
// 健康检查
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
// 应用可观测性中间件
handler := stack.middleware.InstrumentHTTP(mux)
handler = middlewareRequestID(handler)
// 启动HTTP服务器
server := &http.Server{
Addr: ":8080",
Handler: handler,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
}
// 优雅关闭
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
shutdownCtx, shutdownCancel := context.WithTimeout(
context.Background(), 10*time.Second,
)
defer shutdownCancel()
stack.logger.InfoContext(ctx, "shutting down server...")
server.Shutdown(shutdownCtx)
stack.Shutdown(shutdownCtx)
}()
stack.logger.InfoContext(ctx, "server starting",
"addr", server.Addr,
"service", "user-service",
)
if err := server.ListenAndServe(); err != http.ErrServerClosed {
stack.logger.ErrorContext(ctx, "server error", "error", err.Error())
os.Exit(1)
}
}
Grafana仪表盘配置要点:
# docker-compose.yaml - 完整可观测性栈
version: '3.8'
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:0.96.0
command: ["--config=/etc/otelcol/config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otelcol/config.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
prometheus:
image: prom/prometheus:v2.50.0
volumes:
- ./prometheus.yaml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
loki:
image: grafana/loki:2.9.4
ports:
- "3100:3100"
tempo:
image: grafana/tempo:2.3.1
command: ["-config.file=/etc/tempo/tempo.yaml"]
volumes:
- ./tempo.yaml:/etc/tempo/tempo.yaml
ports:
- "3200:3200"
grafana:
image: grafana/grafana:10.3.3
environment:
- GF_AUTH_ANONYMOUS_ENABLED=true
- GF_AUTH_ANONYMOUS_ORG_ROLE=Admin
volumes:
- ./grafana-datasources.yaml:/etc/grafana/provisioning/datasources/datasources.yaml
- ./grafana-dashboards.yaml:/etc/grafana/provisioning/dashboards/dashboards.yaml
ports:
- "3000:3000"
关键要点:
ObservabilityStack统一管理三大支柱的生命周期,确保优雅关闭- Prometheus + Loki + Tempo + Grafana构成完整的可观测性后端
- Grafana数据源配置中启用TraceID跳转,实现从日志到追踪的一键关联
- 采样率通过配置控制,生产环境建议10%采样,异常链路自动100%采样
常见陷阱
陷阱1:slog的With()方法不会复制属性
// ❌ 错误:With()返回新Logger,原Logger不受影响
logger := slog.Default().With("request_id", "abc123")
logger.Info("message") // 有 request_id
slog.Info("message") // 没有 request_id!
// ✅ 正确:始终使用With()返回的新Logger
baseLogger := slog.Default()
requestLogger := baseLogger.With("request_id", "abc123")
requestLogger.Info("message") // 有 request_id
陷阱2:忘记在HTTP客户端传播追踪上下文
// ❌ 错误:直接使用http.NewRequest,追踪链路断裂
req, _ := http.NewRequest("GET", "http://user-service/users/123", nil)
resp, _ := http.DefaultClient.Do(req)
// ✅ 正确:使用NewRequestWithContext并注入传播器
req, _ := http.NewRequestWithContext(ctx, "GET", "http://user-service/users/123", nil)
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
resp, _ := http.DefaultClient.Do(req)
陷阱3:Histogram桶边界设置不当
// ❌ 错误:使用默认桶边界,无法区分正常和异常延迟
am.httpRequestDuration, _ = m.Float64Histogram("http.server.duration")
// ✅ 正确:自定义桶边界,适配HTTP请求延迟分布
am.httpRequestDuration, _ = m.Float64Histogram(
"http.server.duration",
metric.WithUnit("s"),
)
// 在MeterProvider中配置View自定义桶边界
sdkmetric.WithView(
sdkmetric.NewView(
sdkmetric.Instrument{Name: "http.server.duration"},
sdkmetric.Stream{
Aggregation: sdkmetric.AggregationExplicitBucketHistogram{
Boundaries: []float64{0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10},
},
},
),
)
陷阱4:在热路径中创建大量属性
// ❌ 错误:每次请求都创建新的attribute slice
span.SetAttributes(
attribute.String("user.id", userID),
attribute.String("request.path", path),
attribute.String("user.agent", userAgent),
)
// ✅ 正确:预创建常用属性,减少GC压力
var (
attrUserID = attribute.Key("user.id")
attrReqPath = attribute.Key("request.path")
attrUserAgent = attribute.Key("user.agent")
)
span.SetAttributes(
attrUserID.String(userID),
attrReqPath.String(path),
attrUserAgent.String(userAgent),
)
陷阱5:TracerProvider未优雅关闭导致Span丢失
// ❌ 错误:直接退出进程,BatchSpanProcessor中的Span丢失
func main() {
tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
// ... 程序运行 ...
// 进程退出,未Flush的Span丢失!
}
// ✅ 正确:在退出前调用Shutdown确保所有Span被导出
func main() {
tp := sdktrace.NewTracerProvider(sdktrace.WithBatcher(exporter))
defer func() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
tp.Shutdown(ctx) // 确保所有Span被Flush
}()
// ... 程序运行 ...
}
错误排查
| 错误现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 日志中无trace_id | 未从context提取trace信息 | 检查WithContext是否正确调用 |
确保中间件在日志之前初始化 |
| Prometheus无指标数据 | MeterProvider未注册Reader | 检查/metrics端点是否正常 |
确认prometheus.New()已传入WithReader |
| 追踪链路在服务边界断裂 | 未传播TraceContext | 检查HTTP客户端是否调用Inject |
客户端请求前注入传播器 |
| Span数据延迟出现在Jaeger | BatchSpanProcessor批量发送 | 检查Shutdown是否被调用 |
优雅关闭时调用tp.Shutdown(ctx) |
| 指标值异常偏高 | UpDownCounter未正确递减 | 检查Add(-1)是否在所有路径执行 |
使用defer确保递减 |
| Grafana无法查询Loki日志 | Loki数据源配置错误 | 检查Loki URL和Label配置 | 确认{service="xxx"}标签匹配 |
| 采样率设置后仍全量采集 | 采样器配置被覆盖 | 检查是否有AlwaysSample覆盖 |
使用ParentBased包装采样器 |
| gRPC追踪Span缺失 | 未注册拦截器 | 检查gRPC Server是否添加拦截器 | 使用otelgrpc.ServerInterceptor |
| 日志输出为纯文本非JSON | 使用了默认的TextHandler | 检查slog.New的Handler参数 |
替换为JSONHandler |
| OTLP导出连接超时 | Collector未启动或端口错误 | 检查Collector状态和端口 | 确认4317端口可达 |
进阶优化
1. 自适应采样策略
根据请求特征动态调整采样率:错误请求100%采样,慢请求50%采样,正常请求1%采样。通过ShouldSample接口实现自定义采样逻辑。
2. 日志与追踪自动关联
通过slog.Handler自定义实现,在每条日志中自动注入当前Span的TraceID和SpanID,实现日志到追踪的一键跳转,无需手动传递。
3. 指标基数控制
高基数标签(如user_id)会导致Prometheus内存爆炸。使用attribute.KeyValueFilter或CardinalityLimit配置限制标签基数,防止指标爆炸。
4. Exemplar关联指标与追踪
在Prometheus指标中嵌入Exemplar(包含TraceID的样本),实现从指标图表直接跳转到追踪详情。OpenTelemetry SDK已原生支持Exemplar。
5. 多集群联邦监控
对于多集群部署,使用Prometheus Federation + Thanos实现全局指标视图,Grafana配置多数据源实现跨集群可观测性。
对比
| 维度 | slog + OTel | Zap + Prometheus | Logrus + Jaeger |
|---|---|---|---|
| 日志结构化 | ✅ 标准库原生支持 | ✅ 第三方库 | ⚠️ 需要Hook |
| 指标采集 | ✅ OTel统一SDK | ✅ 原生Prometheus | ❌ 需额外集成 |
| 分布式追踪 | ✅ OTel原生支持 | ❌ 需额外集成 | ✅ Jaeger客户端 |
| 上下文传递 | ✅ Context原生集成 | ⚠️ 需手动传递 | ❌ 不支持 |
| 采样控制 | ✅ 内置多种采样器 | ❌ 不适用 | ⚠️ 有限支持 |
| 标准兼容性 | ✅ W3C/CNCF标准 | ⚠️ Prometheus专属 | ⚠️ Jaeger专属 |
| 维护成本 | ✅ 标准库+CNCF维护 | ⚠️ 社区维护 | ❌ 已停止维护 |
| 学习曲线 | ⚠️ OTel概念较多 | ✅ 简单直接 | ✅ 简单 |
总结
2026年的Go可观测性,slog + OpenTelemetry已经成为事实标准。不要再用
fmt.Println调试生产问题,不要让日志和指标割裂,不要让追踪链路在服务边界断裂。从结构化日志开始,逐步接入指标和追踪,最终构建三大支柱统一的可观测性体系——这才是Go服务生产化的正确姿势。
在线工具推荐
本站提供浏览器本地工具,免注册即可试用 →