go中实现日志级别与切割,日志配置热生效,pprof的技术解析

引言

在线上分布式系统和微服务架构中,日志记录是排查问题、调试程序和监控服务运行状态的重要手段。合理设置日志级别,可以帮助开发和运维人员有效地获取所需信息。然而,在实际运行中,常常需要在不重启服务的情况下动态调整日志级别,以适应不同的调试需求和运行环境。本文基于go项目将介绍如何在不重启 funcMaster 服务的情况下,实现日志级别的动态切换。

实现方案

1. 需求分析

目标是在 funcMaster 服务中实现日志级别的动态调整,即不需要重启服务即可改变日志输出的详细程度。为此,需要实现以下功能:

  1. 配置文件读取:服务启动时读取配置文件,获取初始日志级别。
  2. 日志管理:实现一个灵活的日志管理器,根据配置文件中的日志级别动态调整日志输出。
  3. 配置更新:通过外部工具发送新的配置,并在服务中接收和应用该配置。

2. 主要技术点

为了实现上述功能,主要使用以下技术和方法:

  • TCP 通信:服务通过 TCP 监听配置更新请求。
  • 动态调整日志级别:日志管理器根据新的配置动态更新日志级别。
  • 热加载配置:外部工具发送新的配置,服务接收到后立即应用。

3. 代码实现

3.1 主服务 funcMaster.go

主服务负责读取初始配置、启动日志管理器、监听配置更新请求,并根据新的配置动态调整日志级别。

package main

import (
    // Import necessary packages
    _ "net/http/pprof" 
  	_ "github.com/mkevac/debugcharts"
)

// 配置信息结构体
type SotConfig struct {
    // 定义服务的各项配置参数
    LogParam LogParam `json:"logParam"`
}

// 日志参数结构体
type LogParam struct {
    LogLevelStr string `json:"logLevel"`
    LogLevel    int
    LogPath     string `json:"logPath"`
    MaxSize     int    `json:"maxSize"`
    MaxBackups  int    `json:"maxBackups"`
    MaxAge      int    `json:"maxAge"`
}

// 服务主结构体
type FuncMaster struct {
    sotConfig SotConfig
    log       *UULogger
}

// 主函数
func main() {
    cmd := ""
    if len(os.Args) > 1 {
        cmd = os.Args[1]
    }

    if cmd == "start" {
        path := os.Args[2]
        status := startSotByPath(path)
        fmt.Println(status)
    } else {
        info := `
funcMaster is a module for tunneling A datagrams over a B stream.
Usage:
    funcMaster <command> [arguments]
The commands are:
    start       start funcMaster module by config path, return: [0,1,2,3]. 
`
        fmt.Println(info)
    }
}

// 读取配置文件并启动服务
func startSotByPath(configPath string) int32 {
    configData, err := ioutil.ReadFile(configPath)
    if err != nil {
        fmt.Println("error reading configPath")
        return -1
    }
    return startSot(string(configData))
}

// 解析配置并启动服务
func startSot(config string) int32 {
    var sotConfig SotConfig
    err := json.Unmarshal([]byte(config), &sotConfig)
    if err != nil {
        fmt.Printf("SotConfig parse error:%v\n", err)
        return Start_Error_Config
    }

    // 初始化日志
    var logger *UULogger
    logger = NewFileLogger(&sotConfig.LogParam, fmt.Sprintf("(%s) ", sotConfig.LogParam.LogLevelStr))

    // debug,启动pprof工具
    if sotConfig.LogParam.LogLevelStr == "debug" {
        logger.startPprof()
    }
    //清理单例
	clearSingleSot()
    // 启动配置服务器
    server := getSingleSot(sotConfig, logger)
    go startConfigServer(server)

    // 启动server服务
    ret := server.start()
    if ret == 0 {
        logger.Info("server已启动 ret:0\n")
    } else {
        logger.Info("server失败,错误码:%d\n", ret)
    }
    return ret
}

var singleInstance *FuncMaster 
// 单例构建锁
var mu sync.Mutex
// 清理单例
func clearSingleSot() {
	mu.Lock()
	if singleInstance != nil {
		singleInstance.stop()
		singleInstance = nil
	}
	mu.Unlock()
}

func getSingleSot(sotConfig SotConfig, logger *UULogger) *FuncMaster {
	if singleSotInstance == nil {
		singleSotInstance = &FuncMaster {sotConfig: sotConfig, log: logger}
	}
	return singleSotInstance
}

// 配置服务器监听
func startConfigServer(server *FuncMaster) {
    listener, err := net.Listen("tcp", "127.0.0.1:7777")
    if err != nil {
        fmt.Println("Error starting config server:", err)
        return
    }
    defer listener.Close()

    for {
        conn, err := listener.Accept()
        if err != nil {
            fmt.Println("Error accepting connection:", err)
            continue
        }
        go handleConfigConnection(conn, server)
    }
}

// 处理配置更新请求
func handleConfigConnection(conn net.Conn, server *FuncMaster ) {
    defer conn.Close()
    var newConfig SotConfig
    err := json.NewDecoder(conn).Decode(&newConfig)
    if err != nil {
        fmt.Println("Error decoding config:", err)
        return
    }

    server.sotConfig.LogParam = newConfig.LogParam
    server.log.SetLogLevel(newConfig.LogParam.LogLevelStr)
    response := fmt.Sprintf("Log level updated to %s successfully", newConfig.LogParam.LogLevelStr)
    err = json.NewEncoder(conn).Encode(response)
    if err != nil {
        fmt.Println("Error encoding response:", err)
        return
    }
}

3.2 日志管理器 logger.go

日志管理器负责日志记录,并提供动态调整日志级别的功能。

package main

import (
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
    "strings"
    "time"

    "gopkg.in/natefinch/lumberjack.v2"
)

const (
    DEBUG = iota
    INFO
    WARN
    ERROR
    FATAL
)

const (
    ENV_LOG_FILE = "/var/log/funcMaster.log"
)

type UULogger struct {
    logger      *log.Logger
    logLevel    int
    logLevelStr string
    prepend     string
    pprofServer *http.Server
}

func NewFileLogger(logParam *LogParam, prepend string) *UULogger {
    logFileName := logParam.LogPath

    if logFileName == "" {
        logFileName = ENV_LOG_FILE
        fmt.Printf("Default logs path:%s\n", ENV_LOG_FILE)
    } else {
        fileDir, err := os.Stat(logFileName)
        if err == nil && fileDir.IsDir() {
            logFileName = ENV_LOG_FILE
            fmt.Printf("Default logs path:%s\n", ENV_LOG_FILE)
        }
    }

    infoLumberIO := &lumberjack.Logger{
        Filename:   logFileName,
        MaxSize:    logParam.MaxSize,
        MaxBackups: logParam.MaxBackups,
        MaxAge:     logParam.MaxAge,
        LocalTime:  true,
        Compress:   true,
    }

    var uulog UULogger
    logger := log.New(io.MultiWriter(infoLumberIO), "["+logParam.LogLevelStr+"] "+prepend, log.Ldate|log.Ltime)
    uulog.logger = logger
    uulog.prepend = prepend
    uulog.logLevel = logParam.LogLevel
    uulog.logLevelStr = logParam.LogLevelStr
    return &uulog
}

func (log *UULogger) Debug(format string, v ...interface{}) {
    if log.logLevel <= DEBUG {
        log.logger.SetPrefix("[debug] " + log.prepend)
        log.logger.Printf(format, v...)
    }
}

func (log *UULogger) Info(format string, v ...interface{}) {
    if log.logLevel <= INFO {
        log.logger.SetPrefix("[info] " + log.prepend)
        log.logger.Printf(format, v...)
    }
}

func (log *UULogger) Warning(format string, v ...interface{}) {
    if log.logLevel <= WARN {
        log.logger.SetPrefix("[warn] " + log.prepend)
        log.logger.Printf(format, v...)
    }
}

func (log *UULogger) Error(format string, v ...interface{}) {
    if log.logLevel <= ERROR {
        log.logger.SetPrefix("[error] " + log.prepend)
        log.logger.Printf(format, v...)
    }
}

func (log *UULogger) Fatal(format string, v ...interface{}) {
    if log.logLevel <= FATAL {
        log.logger.SetPrefix("[fatal] " + log.prepend)
        log.logger.Printf(format, v...)
    }
}
//启动性能跟踪工具pprof服务
func (log *UULogger) startPprof() {
    if log.pprofServer == nil {
        addr := "0.0.0.0:6060"
        log.pprofServer = &http.Server{Addr: addr}
        go func() {
            log.Debug("pprof 服务器启动,地址:%s", addr)
            err := log.pprofServer.ListenAndServe()
            if err != nil && err != http.ErrServerClosed {
                log.Debug("pprof 服务器启动失败 %v", err)
            }
        }()
    }
}

//停止性能跟踪工具pprof服务
func (log *UULogger) stopPprof() {
    if log.pprofServer != nil {
        log.Debug("pprof 服务器停止")
        err := log.pprofServer.Close()
        if err != nil {
            log.Debug("pprof 服务器停止失败 %v", err)
        }
        log.pprofServer = nil
    }
}

func (log *UULogger) SetLogLevel(leverStr string) {
    log.logLevelStr = strings.ToUpper(leverStr)
    switch log.logLevelStr {
    case "DEBUG":
        log.logLevel = DEBUG
        log.startPprof()
    case "INFO":
        log.logLevel = INFO
        log.stopPprof()
    case "WARN":
        log.logLevel = WARN
        log.stopPprof()
    case "ERROR":
        log.logLevel = ERROR
        log.stopPprof()
    case "FATAL":
        log.logLevel = FATAL
        log.stopPprof()
    default:
        log.logLevel = INFO
        log.stopPprof()
    }

    log.logger.SetPrefix("[" + log.logLevelStr + "] " + log.prepend)
    log.logger.Output(2, "Log level updated to "+log.logLevelStr)
}

3.3 配置更新工具 updateConfig.go

配置更新工具负责读取新的配置文件,通过 TCP 发送给主服务。

package main

import (
    "encoding/json"
    "fmt"
    "net"
    "os"
)

type NewLogParam struct {
    LogLevelStr string `json:"logLevel"`
    LogLevel    int
    LogPath     string `json:"logPath"`
    MaxSize     int    `json:"maxSize"`
    MaxBackups  int    `json:"maxBackups"`
    MaxAge      int    `json:"maxAge"`
}

type NewSotConfig struct {
    LogParam NewLogParam `json:"logParam"`
}

// 验证日志级别是否有效
func isValidLogLevel(logLevel string) bool {
    validLogLevels := []string{"DEBUG", "INFO", "WARN", "ERROR", "FATAL", "debug", "info", "warn", "error", "fatal"}
    for _, validLevel := range validLogLevels {
        if logLevel == validLevel {
            return true
        }
    }
    return false
}

// go build -o updateConfig updateConfig.go
func main() {
    if len(os.Args) < 2 {
        fmt.Println("Usage: updateConfig <configFilePath>")
        return
    }

    configFilePath := os.Args[1]
    configData, err := os.ReadFile(configFilePath)
    if err != nil {
        fmt.Printf("Error reading config file: %v\n", err)
        return
    }

    var newConfig NewSotConfig
    err = json.Unmarshal(configData, &newConfig)
    if err != nil {
        fmt.Printf("Error parsing config file: %v\n", err)
        return
    }

    // 检查日志级别是否有效
    if !isValidLogLevel(newConfig.LogParam.LogLevelStr) {
        fmt.Printf("Invalid log level: %s. Valid log levels are: debug, info, warn, error, fatal, DEBUG, INFO, WARN, ERROR, FATAL\n", newConfig.LogParam.LogLevelStr)
        return
    }

    conn, err := net.Dial("tcp", "127.0.0.1:7777")
    if err != nil {
        fmt.Printf("Error connecting to server: %v\n", err)
        return
    }
    defer conn.Close()

    err = json.NewEncoder(conn).Encode(newConfig)
    if err != nil {
        fmt.Printf("Error sending config: %v\n", err)
        return
    }

    var response string
    err = json.NewDecoder(conn).Decode(&response)
    if err != nil {
        fmt.Printf("Error receiving response: %v\n", err)
        return
    }

    fmt.Println("Response from server:", response)
}

4. 使用说明

4.1 编译项目
go build -o funcMaster funcMaster.go logger.go
go build -o updateConfig updateConfig.go

4.2 运行主服务

创建配置文件 funcMaster.config:

{
    "logParam": {
        "logLevel": "info",
        "logPath": "/var/log/funcMaster.log",
        "maxSize": 100,
        "maxBackups": 30,
        "maxAge": 30
    }
}

启动主服务:

./funcMaster start funcMaster.config

4.3 更新日志配置

编辑配置文件 funcMaster.config,修改 logLevel 字段,例如改为 debug:

./updateConfig funcMaster.config

如果配置正确,输出将显示:

Response from server: Log level updated to debug successfully

5. 总结

通过以上技术实现,可以在不重启服务的情况下,动态调整 funcMaster 服务的日志级别。
1. 提供了日志级别组件,包含第三方组件 Lumberjack 实现日志切割
2. debug下提供go性能工具pprof服务
3. 日志级别设置热生效

这不仅提高了调试的灵活性,还减少了服务中断的风险。
希望这篇文章能对您有所帮助,在您的项目中也能实现类似的功能。

  • 18
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

平行宇宙无敌小坏蛋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值