代码地址:https://gitee.com/lymgoforIT/risk_engine
一 、配置文件加载
目录结构如下:
.
├── LICENSE
├── README.en.md
├── README.md
├── cmd
│ └── risk_engine
│ ├── config.yaml # 配置文件
│ └── engine.go # 入口文件 main入口
├── configs
│ └── config.go # 配置结构体以及加载配置文件的函数
├── global
│ └── config.go # 保存配置的全局变量
├── go.mod
└── go.sum
在服务启动时,一般第一步就是加载相关的配置文件,如服务启动的端口号,DB、Redis
等服务地址,日志相关配置等。
本项目配置就只需要服务配置和应用配置。yaml
配置文件内容如下:
risk_engine/cmd/risk_engine/config.yaml
Server:
Env: dev
Port: 8889
ReadTimeout: 10
WriteTimeout: 10
App:
LogMethod: console
LogPath: ./log/risk_engine.log
DslLoadMethod: file
DslLoadPath: demo/ #注意实际放置目录
对应的结构体与加载函数如下:
risk_engine/configs/config.go
package configs
import (
"os"
"time"
yaml "gopkg.in/yaml.v2"
)
type Conf struct {
Server ServerConf `yaml:"Server"` // 服务相关配置
App AppConf `yaml:"App"` // 应用相关配置
}
type ServerConf struct {
Env string `yaml:"Env"`
Port int `yaml:"Port"`
ReadTimeout time.Duration `yaml:"ReadTimeout"`
WriteTimeout time.Duration `yaml:"WriteTimeout"`
}
type AppConf struct {
LogMethod string `yaml:"LogMethod"`
LogPath string `yaml:"LogPath"`
DslLoadMethod string `yaml:"DslLoadMethod"`
DslLoadPath string `yaml:"DslLoadPath"`
}
// LoadConfig 程序启动时的第一个操作,加载配置文件
func LoadConfig(path string) (*Conf, error) {
conf := new(Conf)
file, err := os.ReadFile(path)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(file, conf)
if err != nil {
return nil, err
}
return conf, nil
}
//策略
type Strategy struct {
Name string `yaml:"name"`
Priority int `yaml:"priority"` //越大越优先
Score int `yaml:"score"` //策略分
}
//keywords for execute
const (
CONSOLE = "console"
FILE = "file"
DB = "db"
PARALLEL = "parallel"
)
为了后续可以很方便的获取到配置信息,我们专门定义了全局变量,用于存储配置信息。
risk_engine/global/config.go
package global
import "github.com/liyouming/risk_engine/configs"
var (
ServerConf *configs.ServerConf
AppConf *configs.AppConf
)
最后,就是服务启动时,将配置文件的信息加载,保存到全局变量中
risk_engine/cmd/risk_engine/engine.go
package main
import (
"context"
"flag"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/liyouming/risk_engine/configs"
"github.com/liyouming/risk_engine/global"
)
func main() {
c := flag.String("c", "", "config file path")
flag.Parse()
conf, err := configs.LoadConfig(*c)
if err != nil {
panic(err) // 加载配置文件失败,直接退出,因为后续操作无意义了
}
global.ServerConf = &conf.Server
global.AppConf = &conf.App
//graceful restart
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
<-quit
log.Println("shutting down server...")
// 上面接受到退出信号后,5s后退出
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case <-ctx.Done():
log.Println("timeout of 5 seconds")
}
log.Println("server exiting")
}
二、日志模块
在项目中,日志是不可获取的组件,我们后续万一有问题,最有效的手段就是根据日志排查各种问题。
这里我们定义了Logger接口,并定义了私有变量以及初始化它的方法,随后提供了所有需要用的日志记录的包级别方法,这些方法内部都是这个私有变量在工作,如下:
risk_engine/internal/log/log.go
package log
import (
"log"
"os"
"github.com/liyouming/risk_engine/configs"
"gopkg.in/natefinch/lumberjack.v2"
)
const (
flag = log.Ldate | log.Ltime | log.Lshortfile
calldepth = 3
)
type Level int
const (
LevelError Level = iota
LevelWarn
LevelInfo
LevelDebug
LevelMax
)
func (l Level) String() string {
switch l {
case LevelError:
return "[ERROR]"
case LevelWarn:
return "[WARN]"
case LevelInfo:
return "[INFO]"
case LevelDebug:
return "[DEBUG]"
}
return ""
}
// interface
type Logger interface {
Error(v ...interface{})
Errorf(format string, v ...interface{})
Warn(v ...interface{})
Warnf(format string, v ...interface{})
Info(v ...interface{})
Infof(format string, v ...interface{})
Debug(v ...interface{})
Debugf(format string, v ...interface{})
}
// logger
var l Logger
func Error(v ...interface{}) {
l.Error(v)
}
func Errorf(format string, v ...interface{}) {
l.Errorf(format, v)
}
func Warn(v ...interface{}) {
l.Warn(v)
}
func Warnf(format string, v ...interface{}) {
l.Warnf(format, v)
}
func Info(v ...interface{}) {
l.Info(v)
}
func Infof(format string, v ...interface{}) {
l.Infof(format, v)
}
func Debug(v ...interface{}) {
l.Debug(v)
}
func Debugf(format string, v ...interface{}) {
l.Debugf(format, v)
}
// init logger, in file
func InitLogger(outputMethod, path string) {
if outputMethod == configs.FILE {
l = NewDefaultLogger(&lumberjack.Logger{
Filename: path,
MaxSize: 500,
MaxAge: 10,
LocalTime: true,
}, "", flag, LevelDebug)
} else { //default
l = NewDefaultLogger(os.Stdout, "", flag, LevelInfo)
}
}
接着,我们需要写一个Logger接口的实现类
risk_engine/internal/log/default_logger.go
package log
import (
"context"
"fmt"
"io"
"log"
)
type defaultLogger struct {
*log.Logger // 内嵌标准库的Logger,那么就相当于继承了标准库Logger的所有方法
writer [LevelMax]outputFn // 记录了每个日志级别的处理函数,打印对应级别的时候,取出对应索引的函数执行
ctx context.Context
}
func NewDefaultLogger(writer io.Writer, prefix string, flag int, level Level) *defaultLogger {
l := &defaultLogger{}
l.Logger = log.New(writer, prefix, flag)
for i := int(LevelError); i < int(LevelMax); i++ {
if i <= int(level) {
l.writer[i] = l.Output // log标准库的Output方法
} else {
l.writer[i] = dropOutput // 低于设置的级别时,啥也不做
}
}
return l
}
type outputFn func(calldepth int, s string) error
func dropOutput(calldepth int, s string) error {
return nil
}
func header(prefix, msg string) string {
return fmt.Sprintf("%s: %s", prefix, msg)
}
func (l *defaultLogger) Error(v ...interface{}) {
l.writer[int(LevelError)](calldepth, header(LevelError.String(), fmt.Sprint(v...)))
}
func (l *defaultLogger) Errorf(format string, v ...interface{}) {
l.writer[int(LevelError)](calldepth, header(LevelError.String(), fmt.Sprintf(format, v...)))
}
func (l *defaultLogger) Warn(v ...interface{}) {
l.writer[int(LevelWarn)](calldepth, header(LevelWarn.String(), fmt.Sprint(v...)))
}
func (l *defaultLogger) Warnf(format string, v ...interface{}) {
l.writer[int(LevelWarn)](calldepth, header(LevelWarn.String(), fmt.Sprintf(format, v...)))
}
func (l *defaultLogger) Info(v ...interface{}) {
l.writer[int(LevelInfo)](calldepth, header(LevelInfo.String(), fmt.Sprint(v...)))
}
func (l *defaultLogger) Infof(format string, v ...interface{}) {
l.writer[int(LevelInfo)](calldepth, header(LevelInfo.String(), fmt.Sprintf(format, v...)))
}
func (l *defaultLogger) Debug(v ...interface{}) {
l.writer[int(LevelDebug)](calldepth, header(LevelDebug.String(), fmt.Sprint(v...)))
}
func (l *defaultLogger) Debugf(format string, v ...interface{}) {
l.writer[int(LevelDebug)](calldepth, header(LevelDebug.String(), fmt.Sprintf(format, v...)))
}
我们可以测试一下日志模块
risk_engine/internal/log/logger_test.go
package log
import (
"testing"
)
func TestLog(t *testing.T) {
//InitLogger("console", "")
InitLogger("file", "./out")
Errorf("this is error %s", "aa")
Error("this is error!")
Debug("this is debug!")
Warn("this is warn!")
Info("this is info!")
}
运行后输出如下,因为我们这里选择输出到文件,所以本地会产生一个out文件
最后,我们可以将日志的初始化,也放到服务启动入口main
方法中
package main
import (
"context"
"flag"
"github.com/liyouming/risk_engine/internal/log"
"os"
"os/signal"
"syscall"
"time"
"github.com/liyouming/risk_engine/configs"
"github.com/liyouming/risk_engine/global"
)
func main() {
c := flag.String("c", "", "config file path")
flag.Parse()
conf, err := configs.LoadConfig(*c)
if err != nil {
panic(err) // 加载配置文件失败,直接退出,因为后续操作无意义了
}
global.ServerConf = &conf.Server
global.AppConf = &conf.App
// 初始化日志模块
log.InitLogger(global.AppConf.LogMethod,global.AppConf.LogPath)
//graceful restart
quit := make(chan os.Signal)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
<-quit
log.Info("shutting down server...")
// 上面接受到退出信号后,5s后退出
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case <-ctx.Done():
log.Info("timeout of 5 seconds")
}
log.Info("server exiting")
}