引言
在线上分布式系统和微服务架构中,日志记录是排查问题、调试程序和监控服务运行状态的重要手段。合理设置日志级别,可以帮助开发和运维人员有效地获取所需信息。然而,在实际运行中,常常需要在不重启服务的情况下动态调整日志级别,以适应不同的调试需求和运行环境。本文基于go项目将介绍如何在不重启 funcMaster 服务的情况下,实现日志级别的动态切换。
实现方案
1. 需求分析
目标是在 funcMaster 服务中实现日志级别的动态调整,即不需要重启服务即可改变日志输出的详细程度。为此,需要实现以下功能:
- 配置文件读取:服务启动时读取配置文件,获取初始日志级别。
- 日志管理:实现一个灵活的日志管理器,根据配置文件中的日志级别动态调整日志输出。
- 配置更新:通过外部工具发送新的配置,并在服务中接收和应用该配置。
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. 日志级别设置热生效
这不仅提高了调试的灵活性,还减少了服务中断的风险。
希望这篇文章能对您有所帮助,在您的项目中也能实现类似的功能。