智能风控决策引擎系统代码实现篇(八)配置文件加载与日志模块

代码地址: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")
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值