📚 原创系列: “Go语言学习系列”
🔗 关注原创: 欢迎扫描文末二维码,关注"Gopher部落"微信公众号获取第一手Go技术文章。
📑 Go语言学习系列导航
🚀 第三阶段:进阶篇本文是【Go语言学习系列】的第43篇,当前位于第三阶段(进阶篇)
- 并发编程(一):goroutine基础
- 并发编程(二):channel基础
- 并发编程(三):select语句
- 并发编程(四):sync包
- 并发编程(五):并发模式
- 并发编程(六):原子操作与内存模型
- 数据库编程(一):SQL接口
- 数据库编程(二):ORM技术
- Web开发(一):路由与中间件
- Web开发(二):模板与静态资源
- Web开发(三):API开发
- Web开发(四):认证与授权
- Web开发(五):WebSocket
- 微服务(一):基础概念
- 微服务(二):gRPC入门
- 日志与监控 👈 当前位置
- 第三阶段项目实战:微服务聊天应用
📖 文章导读
在本文中,您将了解:
- Go语言中日志系统的设计与实现
- 结构化日志与日志最佳实践
- 应用性能指标监控与收集
- 分布式追踪系统的实现
- 可观测性三大支柱:日志、指标、追踪
- 如何选择合适的工具并与云原生生态系统集成
- 实际案例:构建完整的Go应用监控系统
日志与监控
在构建现代应用程序,特别是微服务架构中,日志和监控系统是保证应用可靠性、可维护性的关键组件。本文将探讨如何在Go应用中实现高效的日志记录和全面的系统监控,帮助开发者构建可观测的应用系统。
1. 日志系统
1.1 为什么需要日志系统?
日志是了解应用行为的窗口,提供了应用运行时的历史记录,在以下场景中尤为重要:
- 故障排查:当应用出现问题时,日志是第一手资料
- 性能分析:通过记录关键操作的时间,可以识别性能瓶颈
- 安全审计:记录用户操作和系统事件,帮助发现异常行为
- 行为分析:了解用户如何使用应用,进行产品优化
- 合规要求:满足行业规范和法规对数据留存的要求
在微服务架构中,日志的重要性更加凸显。由于系统组件分布在不同服务中,跟踪请求流程、定位问题变得更加复杂,此时集中式的日志系统成为必要的基础设施。
1.2 Go标准库日志包
Go语言标准库提供了基础的日志功能,通过log
包可以快速实现日志记录:
package main
import (
"log"
"os"
)
func main() {
// 默认日志输出到标准错误
log.Println("这是一条标准日志消息")
// 创建日志文件
f, err := os.OpenFile("app.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
defer f.Close()
// 配置日志输出到文件
logger := log.New(f, "APP: ", log.Ldate|log.Ltime|log.Lshortfile)
// 使用配置好的logger记录日志
logger.Println("应用启动成功")
logger.Printf("配置加载完成: %s", "config.json")
// 记录致命错误并终止程序
if err != nil {
logger.Fatalf("无法连接数据库: %v", err)
}
}
标准库log
包的主要特点:
- 简单易用:API简洁,容易上手
- 支持基本格式化:时间、文件名、行号等
- 支持日志级别:Print、Fatal、Panic
- 可自定义输出目标:文件、网络等
- 支持前缀:可为日志添加前缀信息
然而,标准库日志包存在一些局限性:
- 不支持结构化日志
- 日志级别有限
- 没有内置的日志轮转功能
- 缺乏日志过滤和高级格式化选项
1.3 结构化日志
在复杂应用中,我们需要更高级的日志工具,主要是结构化日志。结构化日志将日志信息组织为键值对格式,而不是简单的文本字符串,便于后续处理和分析。
在Go生态中,有多种流行的结构化日志库,如Zap、Logrus、zerolog等。下面我们将介绍其中几个主流库的使用。
1.3.1 Logrus
Logrus是Go中最流行的结构化日志库之一,提供了丰富的功能:
package main
import (
"os"
log "github.com/sirupsen/logrus"
)
func init() {
// 设置日志格式为JSON
log.SetFormatter(&log.JSONFormatter{})
// 输出到标准输出(默认)
log.SetOutput(os.Stdout)
// 设置日志级别
log.SetLevel(log.InfoLevel)
}
func main() {
// 带结构化字段的日志
log.WithFields(log.Fields{
"service": "user-service",
"method": "GetUser",
"user_id": 123,
}).Info("用户请求处理完成")
// 使用WithField添加单个字段
log.WithField("duration_ms", 42).Warn("请求处理时间过长")
// 错误日志
err := someFunction()
if err != nil {
log.WithError(err).Error("操作失败")
}
// 不同日志级别
log.Trace("非常详细的调试信息")
log.Debug("调试信息")
log.Info("一般信息")
log.Warn("警告")
log.Error("错误")
log.Fatal("致命错误") // 记录后调用os.Exit(1)
log.Panic("严重错误") // 记录后触发panic
}
Logrus的主要特点:
- 结构化日志:支持JSON等格式输出
- 字段系统:通过WithFields添加上下文
- Hook支持:可扩展到多种输出目标
- 日志级别:支持7种级别,可过滤
- 兼容标准库:API设计与标准库相似
1.3.2 Zap
Zap是Uber开发的高性能日志库,专注于性能和内存分配优化:
package main
import (
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func main() {
// 生产环境配置
logger, _ := zap.NewProduction()
defer logger.Sync() // 确保所有日志都被刷新
// 使用结构化日志
logger.Info("服务启动成功",
zap.String("service", "payment-api"),
zap.Int("port", 8080),
zap.Duration("startup_time", time.Millisecond*230),
)
// 记录错误
err := someFunction()
if err != nil {
logger.Error("服务调用失败",
zap.Error(err),
zap.String("service", "inventory-service"),
)
}
// 使用Sugar Logger简化API
sugar := logger.Sugar()
sugar.Infof("Sugar模式: 用户 %s 完成了订单 #%d", "张三", 123456)
sugar.Errorw("Sugar结构化日志",
"user", "张三",
"order_id", 123456,
"error", err,
)
// 自定义配置
config := zap.Config{
Level: zap.NewAtomicLevelAt(zap.DebugLevel),
Development: false,
Encoding: "json",
EncoderConfig: zapcore.EncoderConfig{
TimeKey: "time",
LevelKey: "level",
NameKey: "logger",
CallerKey: "caller",
MessageKey: "msg",
StacktraceKey: "stacktrace",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.LowercaseLevelEncoder,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
},
OutputPaths: []string{"stdout", "app.log"},
ErrorOutputPaths: []string{"stderr"},
}
customLogger, _ := config.Build()
defer customLogger.Sync()
customLogger.Info("自定义配置的日志器初始化完成")
}
Zap的主要特点:
- 极高性能:优化的内存分配和CPU使用
- 两种API风格:标准Logger(类型安全)和Sugar Logger(更简洁)
- 结构化日志:以编程方式定义字段
- 多种编码器:JSON、控制台等
- 采样支持:可控制高吞吐量下的日志量
- 钩子与扩展:灵活的扩展点
1.3.3 zerolog
zerolog是另一个注重性能的结构化日志库,其API设计特别优雅:
package main
import (
"os"
"time"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
func main() {
// 配置全局日志
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
zerolog.SetGlobalLevel(zerolog.InfoLevel)
// 控制台格式输出(开发环境友好)
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
// 简单日志
log.Info().Msg("应用程序启动")
// 带字段的结构化日志
log.Debug().
Str("scale", "833 cents").
Float64("interval", 833.09).
Msg("音高调整")
// 子日志器(模块日志)
userLogger := log.With().Str("component", "user-service").Logger()
userLogger.Info().
Int("user_id", 123).
Str("action", "login").
Msg("用户登录成功")
// 错误日志
err := someFunction()
if err != nil {
log.Error().
Err(err).
Str("service", "payment").
Int("order_id", 12345).
Msg("支付处理失败")
}
// 上下文传递
ctx := log.With().Str("request_id", "req-123").Logger().WithContext(context.Background())
// 从上下文获取日志器
log.Ctx(ctx).Info().Msg("处理请求")
// 不同级别
log.Trace().Msg("超详细追踪")
log.Debug().Msg("调试信息")
log.Info().Msg("普通信息")
log.Warn().Msg("警告")
log.Error().Msg("错误")
log.Fatal().Msg("致命错误") // 会调用os.Exit(1)
log.Panic().Msg("严重错误") // 会触发panic
}
zerolog的主要特点:
- 链式API设计:优雅的API接口
- 零内存分配:设计为避免垃圾回收压力
- 上下文传递:与context.Context集成
- JSON优先:专为JSON输出优化
- 人类友好模式:支持控制台友好输出
- 钩子支持:可扩展到多个后端
1.4 日志最佳实践
无论使用哪种日志库,以下最佳实践可以帮助你设计更有效的日志系统:
1.4.1 日志级别使用指南
- TRACE:详细的追踪信息,通常仅在深度调试时启用
- DEBUG:调试信息,帮助开发者理解程序流程
- INFO:一般操作信息,表明应用正常运行
- WARN:潜在问题的警告,不影响主要功能,但需要注意
- ERROR:错误事件,影响功能但应用仍能继续运行
- FATAL/CRITICAL:严重错误,导致应用无法继续运行
在不同环境中应该使用不同的日志级别:
环境 | 推荐日志级别 | 理由 |
---|---|---|
本地开发 | DEBUG 或 TRACE | 开发者需要更多信息来调试问题 |
测试/预发布 | DEBUG | 帮助识别集成问题和边缘情况 |
生产环境 | INFO | 避免过多日志,仅记录重要信息 |
问题排查时 | 临时调整为 DEBUG | 获取更多信息以排查问题 |
1.4.2 结构化日志指南
编写有效的结构化日志,应遵循以下原则:
- 使用明确的字段名:字段名应自描述,如
user_id
而非id
- 消息应简洁明了:消息内容简洁说明发生了什么
- 分离数据和文本:数据放在字段中,而非嵌入到消息文本
// 不好的写法 log.Info().Msg(fmt.Sprintf("用户 %d 购买了商品 %s,价格为 %.2f 元", 123, "手机", 3999.00)) // 好的写法 log.Info(). Int("user_id", 123). Str("product", "手机"). Float64("price", 3999.00). Msg("用户完成购买")
- 包含上下文信息:如请求ID、用户ID、会话ID等
- 错误详情作为字段:记录完整的错误信息和堆栈
- 保持一致性:全应用使用一致的字段名和日志格式
- 避免敏感信息:不记录密码、令牌等敏感信息
1.4.3 日志轮转与管理
日志文件会随时间增长,需要实施日志轮转策略:
- 大小轮转:当日志文件达到特定大小时轮转
- 时间轮转:按日、周或月轮转
- 保留策略:保留特定数量的日志文件或特定时间段的日志
- 压缩策略:压缩旧日志以节省空间
Go中可以使用lumberjack等库实现日志轮转:
package main
import (
"github.com/natefinch/lumberjack"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
func main() {
// 日志轮转配置
logRotator := &lumberjack.Logger{
Filename: "./logs/app.log", // 日志文件路径
MaxSize: 100, // 每个日志文件最大尺寸,单位MB
MaxBackups: 5, // 保留旧日志文件的最大个数
MaxAge: 30, // 保留旧日志文件的最大天数
Compress: true, // 是否压缩旧日志文件
}
// 配置zap日志
core := zapcore.NewCore(
zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
zapcore.AddSync(logRotator),
zap.InfoLevel,
)
logger := zap.New(core)
defer logger.Sync()
// 记录日志
logger.Info("应用启动",
zap.String("version", "1.0.0"),
zap.Int("port", 8080),
)
}
1.4.4 集中式日志
在微服务架构中,集中式日志解决方案至关重要,常见的日志收集堆栈包括:
-
ELK Stack:
- Elasticsearch:存储和索引日志
- Logstash:收集和处理日志
- Kibana:可视化和分析日志
-
PLG Stack:
- Prometheus:收集和存储指标
- Loki:存储和索引日志
- Grafana:可视化日志和指标
-
Fluentd/Fluent Bit:轻量级日志收集工具,可转发到多种后端
Go应用与这些系统集成的一般流程:
- 配置结构化日志:以JSON格式输出日志到文件或标准输出
- 部署日志收集器:在每个服务节点上部署Filebeat、Fluentd等
- 配置日志处理:使用Logstash或类似工具进行解析和丰富
- 存储到集中位置:如Elasticsearch
- 设置可视化和告警:使用Kibana或Grafana进行可视化和告警
1.4.5 上下文和请求追踪
在多服务环境中,追踪请求流程非常重要。结合日志和追踪ID:
package main
import (
"context"
"net/http"
"github.com/google/uuid"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
// 中间件:添加请求ID
func RequestIDMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 获取现有的请求ID或生成新的
requestID := r.Header.Get("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
// 将请求ID添加到响应头
w.Header().Set("X-Request-ID", requestID)
// 将日志器添加到上下文
ctx := r.Context()
logger := log.With().Str("request_id", requestID).Logger()
ctx = logger.WithContext(ctx)
// 使用带日志器的新上下文调用下一个处理器
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// 处理函数:使用上下文中的日志器
func HandleRequest(w http.ResponseWriter, r *http.Request) {
// 从上下文获取日志器
logger := zerolog.Ctx(r.Context())
logger.Info().
Str("method", r.Method).
Str("path", r.URL.Path).
Str("remote_addr", r.RemoteAddr).
Msg("处理请求")
// 处理请求...
w.Write([]byte("请求已处理"))
}
func main() {
// 设置全局日志格式
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout})
// 注册中间件和处理函数
http.Handle("/api/", RequestIDMiddleware(http.HandlerFunc(HandleRequest)))
// 启动服务器
log.Info().Msg("服务器启动在 :8080")
log.Fatal().Err(http.ListenAndServe(":8080", nil)).Msg("服务器启动失败")
}
1.4.6 错误处理与日志
错误处理和日志记录应紧密结合,以提供完整的错误上下文:
package main
import (
"database/sql"
"errors"
"fmt"
"github.com/rs/zerolog/log"
)
func GetUser(id int) (*User, error) {
user, err := db.GetUserByID(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
// 记录并包装错误
log.Debug().
Err(err).
Int("user_id", id).
Msg("用户不存在")
return nil, fmt.Errorf("用户不存在: %w", err)
}
// 记录详细错误信息,但返回给调用者的是简化信息
log.Error().
Err(err).
Int("user_id", id).
Str("db_server", db.Host).
Msg("获取用户数据失败")
return nil, fmt.Errorf("数据库错误: %w", err)
}
return user, nil
}
遵循以下原则:
- 区分预期错误和非预期错误:预期错误(如资源不存在)用Info/Debug级别,非预期错误用Error级别
- 包含上下文信息:记录发生错误时的相关参数
- 错误包装:使用
fmt.Errorf("上下文: %w", err)
保留原始错误 - 敏感信息处理:外部返回错误不应包含敏感信息
- 堆栈跟踪:对于重要错误,记录完整的堆栈信息
2. 监控系统
2.1 监控的重要性
监控系统是应用可观测性的核心组成部分,它提供了应用运行状态的实时和历史视图。一个完善的监控系统有助于:
- 及早发现问题:在用户受影响前检测异常情况
- 性能优化:识别性能瓶颈和资源使用模式
- 容量规划:了解资源使用趋势,进行合理扩容
- 事件关联分析:将系统事件与业务指标关联
- 服务质量保证:监控SLA指标,确保服务级别
在微服务架构中,由于系统组件分布式部署,监控变得更加关键和复杂。每个微服务需要被单独监控,同时还需要有整体系统的视图。
2.2 监控的核心指标类型
有效的监控系统应该覆盖以下四种黄金指标(Four Golden Signals):
- 延迟(Latency):请求处理的耗时
- 流量(Traffic):系统负载,如每秒请求数
- 错误(Errors):失败请求的比率
- 饱和度(Saturation):系统资源的使用程度
除了这四个核心指标外,还应关注:
- 资源使用率:CPU、内存、磁盘、网络等
- 业务指标:注册用户数、交易量等与业务相关的指标
- 应用特定指标:缓存命中率、队列长度等
2.3 Go应用指标收集
2.3.1 标准库的性能指标
Go标准库提供了一些基本的性能统计功能:
package main
import (
"fmt"
"net/http"
"runtime"
"time"
_ "net/http/pprof" // 引入pprof
)
func printStats() {
var memStats runtime.MemStats
for {
// 收集内存统计
runtime.ReadMemStats(&memStats)
fmt.Println("=== 内存统计 ===")
fmt.Printf("Alloc = %v MiB", bToMb(memStats.Alloc))
fmt.Printf("\tTotalAlloc = %v MiB", bToMb(memStats.TotalAlloc))
fmt.Printf("\tSys = %v MiB", bToMb(memStats.Sys))
fmt.Printf("\tNumGC = %v\n", memStats.NumGC)
// 收集goroutine统计
fmt.Printf("Goroutines: %d\n", runtime.NumGoroutine())
time.Sleep(10 * time.Second)
}
}
func bToMb(b uint64) uint64 {
return b / 1024 / 1024
}
func main() {
// 启动pprof HTTP服务器
go func() {
fmt.Println("启动pprof服务器在 :6060")
http.ListenAndServe(":6060", nil)
}()
// 定期打印统计信息
go printStats()
// 主服务逻辑...
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, World!"))
})
http.ListenAndServe(":8080", nil)
}
通过net/http/pprof
,可以获取以下信息:
- CPU分析:
/debug/pprof/profile
- 堆分析:
/debug/pprof/heap
- Goroutine分析:
/debug/pprof/goroutine
- 线程创建分析:
/debug/pprof/threadcreate
- 阻塞分析:
/debug/pprof/block
- 互斥锁分析:
/debug/pprof/mutex
可以使用go工具分析这些数据:
# 收集30秒的CPU分析
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
# 分析内存分配
go tool pprof http://localhost:6060/debug/pprof/heap
# 查看Goroutine
go tool pprof http://localhost:6060/debug/pprof/goroutine
2.3.2 自定义指标与Prometheus
Prometheus是目前最流行的监控系统之一,为Go应用提供了优秀的支持。
使用prometheus/client_golang库可以轻松暴露自定义指标:
package main
import (
"math/rand"
"net/http"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
)
var (
// 定义计数器:记录请求总数
httpRequestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "HTTP请求总数",
},
[]string{"method", "path", "status"},
)
// 定义直方图:记录请求持续时间
httpRequestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP请求处理时间分布",
Buckets: prometheus.DefBuckets, // 默认桶
},
[]string{"method", "path"},
)
// 定义仪表盘:当前活跃请求数
httpRequestsInProgress = promauto.NewGaugeVec(
prometheus.GaugeOpts{
Name: "http_requests_in_progress",
Help: "当前处理中的HTTP请求数",
},
[]string{"method"},
)
// 定义摘要:计算分位数
httpRequestSummary = promauto.NewSummaryVec(
prometheus.SummaryOpts{
Name: "http_request_summary_seconds",
Help: "HTTP请求处理时间摘要",
Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001},
},
[]string{"method", "path"},
)
)
// 中间件:记录请求指标
func metricsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 开始时间
start := time.Now()
// 增加活跃请求计数
httpRequestsInProgress.WithLabelValues(r.Method).Inc()
// 包装ResponseWriter以获取状态码
ww := newResponseWriter(w)
// 处理请求
next.ServeHTTP(ww, r)
// 减少活跃请求计数
httpRequestsInProgress.WithLabelValues(r.Method).Dec()
// 结束时间
duration := time.Since(start).Seconds()
// 记录请求计数
httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, http.StatusText(ww.status)).Inc()
// 记录请求持续时间
httpRequestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
// 记录到摘要
httpRequestSummary.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
})
}
// 实现自定义ResponseWriter以捕获状态码
type responseWriter struct {
http.ResponseWriter
status int
}
func newResponseWriter(w http.ResponseWriter) *responseWriter {
return &responseWriter{w, http.StatusOK}
}
func (rw *responseWriter) WriteHeader(code int) {
rw.status = code
rw.ResponseWriter.WriteHeader(code)
}
func main() {
// 注册Prometheus Handler
http.Handle("/metrics", promhttp.Handler())
// 定义应用路由
appRouter := http.NewServeMux()
appRouter.HandleFunc("/", handleHome)
appRouter.HandleFunc("/api/users", handleUsers)
// 添加度量中间件
http.Handle("/api/", metricsMiddleware(appRouter))
http.Handle("/", metricsMiddleware(appRouter))
// 模拟一些后台活动,生成指标
go generateMetrics()
// 启动服务器
http.ListenAndServe(":8080", nil)
}
func handleHome(w http.ResponseWriter, r *http.Request) {
// 模拟处理时间
time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
w.Write([]byte("Welcome Home!"))
}
func handleUsers(w http.ResponseWriter, r *http.Request) {
// 模拟处理时间
time.Sleep(time.Duration(rand.Intn(200)) * time.Millisecond)
// 模拟错误
if rand.Float64() < 0.1 {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte("Internal Server Error"))
return
}
w.Write([]byte("User List"))
}
func generateMetrics() {
// 自定义业务指标
itemsProcessed := promauto.NewCounter(prometheus.CounterOpts{
Name: "app_items_processed_total",
Help: "处理的项目总数",
})
queueSize := promauto.NewGauge(prometheus.GaugeOpts{
Name: "app_queue_size",
Help: "当前队列长度",
})
// 模拟指标变化
for {
// 增加计数器
itemsProcessed.Inc()
// 更新仪表盘
queueSize.Set(float64(rand.Intn(100)))
time.Sleep(time.Second)
}
}
2.3.3 Prometheus指标类型
Prometheus支持四种主要的指标类型:
-
Counter(计数器):
- 单调递增的计数器
- 用于记录事件总数、请求总数、错误总数等
- 只能增加,不能减少
- 例子:
http_requests_total
、errors_total
-
Gauge(仪表盘):
- 可增可减的数值
- 用于记录当前值,如内存使用、活跃连接数
- 例子:
memory_usage_bytes
、active_connections
-
Histogram(直方图):
- 对观察值进行采样,并按照可配置的桶来计数
- 用于请求持续时间、响应大小等分布式数据
- 自动生成
_count
、_sum
和_bucket
系列指标 - 例子:
http_request_duration_seconds
-
Summary(摘要):
- 类似直方图,但直接计算分位数
- 支持百分位(例如p99)计算
- 例子:
http_request_duration_seconds_summary
2.3.4 Prometheus配置与服务发现
简单的Prometheus配置示例(prometheus.yml):
global:
scrape_interval: 15s # 采集间隔
evaluation_interval: 15s # 规则评估间隔
scrape_configs:
- job_name: 'go-application'
static_configs:
- targets: ['localhost:8080'] # 应用暴露指标的地址
labels:
service: 'user-service' # 自定义标签
env: 'production'
# 使用服务发现
- job_name: 'kubernetes-pods'
kubernetes_sd_configs:
- role: pod
relabel_configs:
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
action: keep
regex: true
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
action: replace
target_label: __metrics_path__
regex: (.+)
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
action: replace
regex: ([^:]+)(?::\d+)?;(\d+)
replacement: $1:$2
target_label: __address__
2.4 可视化与告警
2.4.1 Grafana
Grafana是一个流行的开源可视化工具,可以与Prometheus等数据源集成,创建丰富的仪表板。
Go应用的Grafana仪表板示例:
-
应用概览:
- 请求量和错误率
- 响应时间分布
- 活跃连接数
-
资源使用:
- CPU和内存使用
- Goroutine数量
- GC统计
-
业务指标:
- 用户活跃度
- 交易量
- 关键业务流程完成率
Grafana仪表板JSON示例(部分):
{
"annotations": {
"list": []
},
"editable": true,
"gnetId": null,
"graphTooltip": 0,
"id": 1,
"links": [],
"panels": [
{
"aliasColors": {},
"bars": false,
"dashLength": 10,
"dashes": false,
"datasource": "Prometheus",
"fieldConfig": {
"defaults": {
"custom": {}
},
"overrides": []
},
"fill": 1,
"fillGradient": 0,
"gridPos": {
"h": 8,
"w": 12,
"x": 0,
"y": 0
},
"hiddenSeries": false,
"id": 2,
"legend": {
"avg": false,
"current": false,
"max": false,
"min": false,
"show": true,
"total": false,
"values": false
},
"lines": true,
"linewidth": 1,
"nullPointMode": "null",
"options": {
"alertThreshold": true
},
"percentage": false,
"pluginVersion": "7.4.3",
"pointradius": 2,
"points": false,
"renderer": "flot",
"seriesOverrides": [],
"spaceLength": 10,
"stack": false,
"steppedLine": false,
"targets": [
{
"expr": "sum(rate(http_requests_total[5m])) by (path)",
"interval": "",
"legendFormat": "{{path}}",
"refId": "A"
}
],
"thresholds": [],
"timeFrom": null,
"timeRegions": [],
"timeShift": null,
"title": "请求速率 (5分钟)",
"tooltip": {
"shared": true,
"sort": 0,
"value_type": "individual"
},
"type": "graph",
"xaxis": {
"buckets": null,
"mode": "time",
"name": null,
"show": true,
"values": []
},
"yaxes": [
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
},
{
"format": "short",
"label": null,
"logBase": 1,
"max": null,
"min": null,
"show": true
}
],
"yaxis": {
"align": false,
"alignLevel": null
}
}
],
"schemaVersion": 27,
"style": "dark",
"tags": [],
"templating": {
"list": []
},
"time": {
"from": "now-6h",
"to": "now"
},
"timepicker": {},
"timezone": "",
"title": "Go应用监控",
"uid": "go-app-monitor",
"version": 1
}
2.4.2 告警配置
Prometheus支持告警功能,可以基于PromQL查询定义告警规则:
# alert_rules.yml
groups:
- name: go-app-alerts
rules:
- alert: HighErrorRate
expr: sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "高错误率请求 (> 5%)"
description: "过去5分钟内有 {{ $value | humanizePercentage }} 的请求返回错误"
- alert: SlowResponseTime
expr: histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le)) > 1
for: 5m
labels:
severity: warning
annotations:
summary: "响应时间过长"
description: "95%的请求响应时间超过1秒"
- alert: HighMemoryUsage
expr: go_memstats_alloc_bytes / go_memstats_sys_bytes > 0.8
for: 10m
labels:
severity: warning
annotations:
summary: "内存使用率高"
description: "应用已使用 {{ $value | humanizePercentage }} 的分配内存"
将告警规则加载到Prometheus配置中:
# prometheus.yml
rule_files:
- "alert_rules.yml"
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
2.5 分布式追踪
在微服务架构中,一个用户请求通常要跨越多个服务。分布式追踪可以帮助我们理解请求如何流经这些服务。
2.5.1 OpenTelemetry
OpenTelemetry是一个开源项目,提供了统一的API、库和代理,用于收集和导出遥测数据(追踪、指标、日志)。
使用OpenTelemetry在Go应用中实现分布式追踪:
package main
import (
"context"
"log"
"net/http"
"time"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.4.0"
"go.opentelemetry.io/otel/trace"
)
var tracer trace.Tracer
func initTracer() func() {
// 创建Jaeger导出器
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(jaeger.WithEndpoint("http://localhost:14268/api/traces")))
if err != nil {
log.Fatalf("failed to create exporter: %v", err)
}
// 创建追踪提供者
tp := tracesdk.NewTracerProvider(
tracesdk.WithSampler(tracesdk.AlwaysSample()),
tracesdk.WithBatcher(exp),
tracesdk.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String("user-service"),
attribute.String("environment", "production"),
)),
)
// 设置全局追踪提供者
otel.SetTracerProvider(tp)
// 获取追踪器
tracer = tp.Tracer("user-service")
// 返回清理函数
return func() {
if err := tp.Shutdown(context.Background()); err != nil {
log.Fatalf("Error shutting down tracer provider: %v", err)
}
}
}
func main() {
// 初始化追踪器
cleanup := initTracer()
defer cleanup()
// 注册HTTP处理函数
http.HandleFunc("/api/users", userHandler)
// 启动HTTP服务器
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func userHandler(w http.ResponseWriter, r *http.Request) {
// 从请求中提取追踪上下文
ctx := r.Context()
// 创建span
ctx, span := tracer.Start(ctx, "user-handler")
defer span.End()
// 设置span的属性
span.SetAttributes(
attribute.String("http.method", r.Method),
attribute.String("http.url", r.URL.Path),
)
// 模拟数据库查询
users, err := queryUsers(ctx)
if err != nil {
span.RecordError(err)
http.Error(w, "Database error", http.StatusInternalServerError)
return
}
// 模拟外部服务调用
userDetails, err := fetchUserDetails(ctx, users[0].ID)
if err != nil {
span.RecordError(err)
http.Error(w, "External service error", http.StatusInternalServerError)
return
}
// 响应
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"success","data":{"users":...}}`))
}
type User struct {
ID int
Name string
}
func queryUsers(ctx context.Context) ([]User, error) {
// 创建子span
_, span := tracer.Start(ctx, "query-users")
defer span.End()
// 模拟数据库查询耗时
time.Sleep(100 * time.Millisecond)
// 添加span事件
span.AddEvent("db-query-complete", trace.WithAttributes(
attribute.Int("found_users", 2),
))
return []User{{ID: 1, Name: "张三"}, {ID: 2, Name: "李四"}}, nil
}
func fetchUserDetails(ctx context.Context, userID int) (map[string]interface{}, error) {
// 创建子span
ctx, span := tracer.Start(ctx, "fetch-user-details")
defer span.End()
span.SetAttributes(attribute.Int("user.id", userID))
// 模拟HTTP调用
time.Sleep(200 * time.Millisecond)
// 模拟错误(10%概率)
if time.Now().UnixNano()%10 == 0 {
err := &httpError{statusCode: 503, message: "Service Unavailable"}
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
return map[string]interface{}{
"address": "北京市朝阳区",
"phone": "13812345678",
}, nil
}
type httpError struct {
statusCode int
message string
}
func (e *httpError) Error() string {
return e.message
}
2.5.2 追踪上下文传播
在微服务环境中,追踪上下文需要在服务间传递,以关联整个请求链路上的span:
package main
import (
"context"
"net/http"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
)
func init() {
// 设置全局传播器
otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
))
}
// 客户端:在HTTP请求中传播追踪上下文
func callService(ctx context.Context, url string) (*http.Response, error) {
// 使用带OpenTelemetry检测的HTTP客户端
client := http.Client{
Transport: otelhttp.NewTransport(http.DefaultTransport),
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
// 发起请求,追踪上下文会自动添加到HTTP头中
return client.Do(req)
}
// 服务端:提取追踪上下文
func tracingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// otelhttp.Handler自动提取追踪上下文并创建span
otelhttp.NewHandler(next, "http-handler").ServeHTTP(w, r)
})
}
func main() {
// 注册带追踪的HTTP处理器
http.Handle("/api/", tracingMiddleware(apiHandler()))
// 启动服务器
log.Fatal(http.ListenAndServe(":8080", nil))
}
func apiHandler() http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// 处理请求...
// 追踪上下文已经在r.Context()中可用
})
}
2.5.3 Instrumenting gRPC
对于gRPC服务,OpenTelemetry提供了专门的拦截器:
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
"example.com/myapp/proto"
)
func main() {
// 创建gRPC服务器
server := grpc.NewServer(
grpc.UnaryInterceptor(otelgrpc.UnaryServerInterceptor()),
grpc.StreamInterceptor(otelgrpc.StreamServerInterceptor()),
)
// 注册gRPC服务
proto.RegisterUserServiceServer(server, &userService{})
// 启动服务器
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
log.Println("gRPC server starting on :50051")
log.Fatal(server.Serve(lis))
}
// gRPC客户端
func createGrpcClient() (proto.UserServiceClient, error) {
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithInsecure(),
grpc.WithUnaryInterceptor(otelgrpc.UnaryClientInterceptor()),
grpc.WithStreamInterceptor(otelgrpc.StreamClientInterceptor()),
)
if err != nil {
return nil, err
}
return proto.NewUserServiceClient(conn), nil
}
2.5.4 可视化追踪数据
追踪数据收集后,可以通过Jaeger UI等工具进行可视化,分析请求流程和性能问题。Jaeger UI提供以下功能:
- 追踪查询:按服务、操作、标签等过滤
- 追踪时间线:查看span的时间分布
- 依赖图:服务间调用关系可视化
- 比较视图:比较不同追踪的差异
2.6 可观测性架构
一个完整的可观测性架构应包含三大支柱:
- 日志(Logs):详细的事件记录
- 指标(Metrics):系统行为的数字表示
- 追踪(Traces):请求流程的可视化
2.6.1 云原生可观测性堆栈
典型的云原生可观测性堆栈包括:
- 日志收集:Fluentd、Fluent Bit、Logstash
- 日志存储:Elasticsearch、Loki
- 指标收集:Prometheus、OpenTelemetry Collector
- 追踪收集:Jaeger、Zipkin、OpenTelemetry Collector
- 可视化:Grafana、Kibana
- 告警:Alertmanager、Grafana Alerting
2.6.2 整合三大支柱
将日志、指标和追踪关联起来,可以提供完整的系统视图:
package main
import (
"context"
"fmt"
"net/http"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/trace"
)
// 提取追踪信息并添加到日志
func logWithTracing(ctx context.Context) zerolog.Logger {
spanCtx := trace.SpanContextFromContext(ctx)
logger := log.With().
Str("trace_id", spanCtx.TraceID().String()).
Str("span_id", spanCtx.SpanID().String())
return logger
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 创建span
ctx, span := otel.Tracer("").Start(ctx, "handle-request")
defer span.End()
// 从上下文获取追踪ID并添加到日志
logger := logWithTracing(ctx)
// 请求开始日志
logger.Info().
Str("method", r.Method).
Str("path", r.URL.Path).
Str("remote_addr", r.RemoteAddr).
Msg("Processing request")
// 执行业务逻辑...
// 记录一些指标
requestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(time.Since(start).Seconds())
// 请求结束日志
logger.Info().
Int("status", 200).
Int64("duration_ms", time.Since(start).Milliseconds()).
Msg("Request completed")
}
3. 实践案例:构建完整的Go应用监控系统
以下是一个综合案例,展示如何为Go微服务应用构建完整的日志和监控系统。
3.1 系统架构
我们将构建的监控系统包括以下组件:
- 应用服务:多个Go微服务,使用结构化日志、暴露Prometheus指标、集成OpenTelemetry追踪
- 日志收集:Fluent Bit收集日志并发送到Loki
- 指标收集:Prometheus抓取应用指标
- 追踪收集:Jaeger收集分布式追踪数据
- 存储:Loki存储日志,Prometheus存储指标,Jaeger存储追踪
- 可视化:Grafana提供统一的可视化界面
- 告警:Alertmanager处理告警并通知
3.2 应用配置
下面是一个Go微服务的配置示例,集成了上述所有可观测性组件:
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/jaeger"
"go.opentelemetry.io/otel/sdk/resource"
tracesdk "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.7.0"
"go.opentelemetry.io/otel/trace"
)
const (
serviceName = "order-service"
)
var (
// 指标定义
requestsTotal = promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "http_requests_total",
Help: "HTTP请求总数",
},
[]string{"method", "path", "status"},
)
requestDuration = promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "http_request_duration_seconds",
Help: "HTTP请求处理时间",
Buckets: []float64{0.01, 0.05, 0.1, 0.5, 1, 2, 5},
},
[]string{"method", "path"},
)
// 全局tracer
tracer trace.Tracer
)
// 初始化追踪器
func initTracer() (func(), error) {
// 配置Jaeger导出器
exp, err := jaeger.New(jaeger.WithCollectorEndpoint(
jaeger.WithEndpoint("http://jaeger:14268/api/traces"),
))
if err != nil {
return nil, fmt.Errorf("创建Jaeger导出器失败: %w", err)
}
// 创建追踪提供者
tp := tracesdk.NewTracerProvider(
tracesdk.WithBatcher(exp),
tracesdk.WithSampler(tracesdk.AlwaysSample()),
tracesdk.WithResource(resource.NewWithAttributes(
semconv.SchemaURL,
semconv.ServiceNameKey.String(serviceName),
semconv.ServiceVersionKey.String("1.0.0"),
)),
)
// 设置全局提供者
otel.SetTracerProvider(tp)
// 获取tracer
tracer = otel.Tracer(serviceName)
// 返回清理函数
return func() {
if err := tp.Shutdown(context.Background()); err != nil {
log.Error().Err(err).Msg("关闭追踪提供者失败")
}
}, nil
}
// 初始化日志
func initLogger() {
// 配置zerolog
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
log.Logger = zerolog.New(os.Stdout).
With().
Timestamp().
Str("service", serviceName).
Logger()
// 开发环境使用控制台格式
if os.Getenv("ENV") == "development" {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339})
}
}
// 中间件:记录请求、指标和追踪
func observabilityMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// 获取追踪上下文
ctx := r.Context()
spanCtx := trace.SpanContextFromContext(ctx)
// 创建日志字段
logCtx := log.With().
Str("method", r.Method).
Str("path", r.URL.Path).
Str("remote_addr", r.RemoteAddr).
Str("trace_id", spanCtx.TraceID().String()).
Str("span_id", spanCtx.SpanID().String())
logger := logCtx.Logger()
// 请求开始日志
logger.Info().Msg("请求开始处理")
// 包装ResponseWriter以捕获状态码
ww := newResponseWriter(w)
// 调用下一个处理器
next.ServeHTTP(ww, r)
// 计算持续时间
duration := time.Since(start).Seconds()
// 记录指标
statusCode := ww.statusCode
if statusCode == 0 {
statusCode = 200 // 默认状态码
}
requestsTotal.WithLabelValues(r.Method, r.URL.Path, fmt.Sprintf("%d", statusCode)).Inc()
requestDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
// 请求完成日志
logger.Info().
Int("status", statusCode).
Float64("duration_sec", duration).
Msg("请求处理完成")
})
}
// 包装ResponseWriter以捕获状态码
type responseWriter struct {
http.ResponseWriter
statusCode int
}
func newResponseWriter(w http.ResponseWriter) *responseWriter {
return &responseWriter{w, 0}
}
func (rw *responseWriter) WriteHeader(code int) {
rw.statusCode = code
rw.ResponseWriter.WriteHeader(code)
}
// 处理函数:订单创建
func handleCreateOrder(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 创建span
ctx, span := tracer.Start(ctx, "create-order")
defer span.End()
// 模拟订单处理
time.Sleep(100 * time.Millisecond)
// 模拟数据库操作
if err := saveOrder(ctx); err != nil {
http.Error(w, "保存订单失败", http.StatusInternalServerError)
return
}
// 模拟发送通知
if err := notifyUser(ctx); err != nil {
// 记录错误但不影响响应
span.RecordError(err)
zerolog.Ctx(ctx).Error().Err(err).Msg("发送通知失败")
}
// 返回成功响应
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"status":"success","order_id":"12345"}`))
}
// 模拟保存订单
func saveOrder(ctx context.Context) error {
_, span := tracer.Start(ctx, "save-order-db")
defer span.End()
// 模拟数据库操作
time.Sleep(50 * time.Millisecond)
// 获取logger并添加上下文
logger := zerolog.Ctx(ctx)
if logger.GetLevel() == zerolog.NoLevel {
logger = &log.Logger
}
logger.Debug().Msg("订单已保存到数据库")
return nil
}
// 模拟通知用户
func notifyUser(ctx context.Context) error {
ctx, span := tracer.Start(ctx, "notify-user")
defer span.End()
// 模拟通知服务调用
time.Sleep(30 * time.Millisecond)
// 随机模拟错误
if time.Now().UnixNano()%10 == 0 {
return fmt.Errorf("通知服务暂时不可用")
}
return nil
}
func main() {
// 初始化日志
initLogger()
// 初始化追踪
cleanup, err := initTracer()
if err != nil {
log.Fatal().Err(err).Msg("初始化追踪失败")
}
defer cleanup()
// 创建HTTP服务器
mux := http.NewServeMux()
// 注册指标端点
mux.Handle("/metrics", promhttp.Handler())
// 注册业务端点,添加追踪
orderHandler := http.HandlerFunc(handleCreateOrder)
mux.Handle("/api/orders",
observabilityMiddleware(
otelhttp.NewHandler(orderHandler, "create-order"),
),
)
// 添加健康检查端点
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"up"}`))
})
// 创建HTTP服务器
srv := &http.Server{
Addr: ":8080",
Handler: mux,
}
// 启动服务器
go func() {
log.Info().Str("addr", srv.Addr).Msg("HTTP服务器启动")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal().Err(err).Msg("HTTP服务器错误")
}
}()
// 优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Info().Msg("正在关闭服务器...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal().Err(err).Msg("服务器强制关闭")
}
log.Info().Msg("服务器已优雅关闭")
}
3.3 部署监控堆栈
使用Docker Compose部署完整的监控堆栈:
# docker-compose.yml
version: '3'
services:
# 应用服务
order-service:
build: ./order-service
ports:
- "8080:8080"
environment:
- JAEGER_ENDPOINT=http://jaeger:14268/api/traces
- ENV=production
depends_on:
- prometheus
- jaeger
- loki
# Prometheus
prometheus:
image: prom/prometheus:v2.37.0
ports:
- "9090:9090"
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
# Alertmanager
alertmanager:
image: prom/alertmanager:v0.24.0
ports:
- "9093:9093"
volumes:
- ./alertmanager/alertmanager.yml:/etc/alertmanager/alertmanager.yml
command:
- '--config.file=/etc/alertmanager/alertmanager.yml'
- '--storage.path=/alertmanager'
# Grafana
grafana:
image: grafana/grafana:9.1.0
ports:
- "3000:3000"
volumes:
- ./grafana/provisioning:/etc/grafana/provisioning
- grafana_data:/var/lib/grafana
environment:
- GF_SECURITY_ADMIN_USER=admin
- GF_SECURITY_ADMIN_PASSWORD=admin
- GF_USERS_ALLOW_SIGN_UP=false
depends_on:
- prometheus
- loki
- jaeger
# Jaeger
jaeger:
image: jaegertracing/all-in-one:1.37
ports:
- "16686:16686" # UI
- "14268:14268" # 收集器HTTP
- "6831:6831/udp" # 代理
environment:
- COLLECTOR_OTLP_ENABLED=true
# Loki
loki:
image: grafana/loki:2.6.1
ports:
- "3100:3100"
command: -config.file=/etc/loki/local-config.yaml
volumes:
- ./loki/local-config.yaml:/etc/loki/local-config.yaml
# Fluent Bit
fluent-bit:
image: fluent/fluent-bit:1.9
volumes:
- ./fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf
- ./fluent-bit/parsers.conf:/fluent-bit/etc/parsers.conf
- /var/log:/var/log
ports:
- "24224:24224"
depends_on:
- loki
volumes:
prometheus_data:
grafana_data:
3.4 监控与告警配置
Prometheus配置:
# prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
rule_files:
- "alert_rules.yml"
alerting:
alertmanagers:
- static_configs:
- targets: ['alertmanager:9093']
scrape_configs:
- job_name: 'order-service'
static_configs:
- targets: ['order-service:8080']
labels:
service: 'order-service'
env: 'production'
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
告警规则:
# alert_rules.yml
groups:
- name: go-services
rules:
- alert: HighErrorRate
expr: sum(rate(http_requests_total{status=~"5.."}[5m])) by (service) / sum(rate(http_requests_total[5m])) by (service) > 0.05
for: 2m
labels:
severity: critical
annotations:
summary: "服务 {{ $labels.service }} 错误率过高"
description: "服务 {{ $labels.service }} 在过去5分钟的错误率为 {{ $value | humanizePercentage }}"
- alert: ServiceDown
expr: up == 0
for: 1m
labels:
severity: critical
annotations:
summary: "服务 {{ $labels.service }} 已宕机"
description: "服务 {{ $labels.service }} 已经宕机超过1分钟"
Fluent Bit配置:
# fluent-bit.conf
[SERVICE]
Flush 1
Log_Level info
Daemon off
[INPUT]
Name tail
Path /var/log/containers/*order-service*.log
Parser docker
Tag kube.*
Mem_Buf_Limit 5MB
[FILTER]
Name parser
Match kube.*
Key_Name log
Parser json
Reserve_Data On
[OUTPUT]
Name loki
Match kube.*
Host loki
Port 3100
Labels job=order-service
Label_Keys $trace_id,$span_id,$level
Grafana仪表板示例:
创建仪表板,整合三个数据源(Prometheus、Loki、Jaeger)的数据,包括:
-
服务健康概览:
- 服务可用性
- 请求速率和错误率
- 响应时间分布
- CPU和内存使用
-
日志面板:
- 按日志级别过滤
- 按追踪ID过滤
- 错误日志高亮
-
服务调用面板:
- 服务间调用拓扑图
- 慢查询追踪列表
- 错误追踪列表
总结
在本文中,我们深入探讨了Go语言中的日志记录和监控系统,这两个方面对于构建可靠、可维护的应用程序至关重要。
我们首先学习了日志记录的基础知识,包括Go标准库log
的使用方法、结构化日志库的优势,以及如何配置和使用zap、logrus等流行的日志库。我们还讨论了日志级别、字段和上下文的重要性,以及日志轮转和聚合的最佳实践。
接着,我们探索了应用程序监控的各个方面,包括系统指标、应用指标和业务指标的收集。我们学习了如何使用Prometheus和Grafana建立监控系统,以及如何实现健康检查和告警机制。我们还讨论了分布式追踪的概念,以及如何使用Jaeger或Zipkin等工具在微服务环境中实现端到端的请求追踪。
最后,我们将这些知识整合在一起,展示了如何构建一个完整的可观测性栈,以及如何根据性能指标和分析数据持续优化应用程序。
通过掌握日志记录和监控技术,你将能够更有效地调试问题、优化性能、预测潜在风险,并确保你的Go应用程序在生产环境中平稳运行。记住,可观测性不仅仅是工具的集合,更是一种文化和实践,贯穿于整个软件开发生命周期。
在接下来的文章中,我们将通过一个完整的微服务聊天应用项目,将我们在整个系列中学到的知识整合起来,展示如何构建一个功能完整、性能优良的Go应用。
👨💻 关于作者与Gopher部落
Gopher部落专注于Go语言技术分享,包括Go语言基础、Web开发、并发编程、微服务架构等。我们提供系统的Go语言学习路径和最佳实践,帮助您成为优秀的Go工程师。
为什么关注我们:
- 系统的Go语言学习路径,从入门到进阶
- 实用的项目实战经验分享
- 深入的技术原理剖析
- 活跃的技术交流社区
如何关注我们:
- 点击CSDN专栏右上角"关注"按钮
- 扫描文末二维码关注"Gopher部落"微信公众号
关注后可获取:
- 完整的Go语言学习路线图
- Go语言面试题PDF
- 项目实战源代码
- 定制化学习计划指导
期待与您在Go语言的学习旅程中共同成长!