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

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

<think>嗯,用户现在问的是规则引擎的设计,我需要详细介绍一下这个主题。首先,我得回想一下之前用户的问题,他们之前问过开源规则引擎的推荐,现在可能想深入了解如何自己设计一个规则引擎。 用户可能是开发者,或者是对系统架构有兴趣的技术人员,他们可能想了解规则引擎的内部机制,以便在自己的项目中应用或者优化现有的解决方案。所以需要从基础概念开始,逐步展开,保持结构清晰。 规则引擎设计的核心部分包括规则定义、规则匹配、规则执行、冲突解决和结果处理。我需要分点解释每个部分,并且结合一些例子,比如用Drools的DRL语法或者EasyRules的注解方式来说明规则定义的不同方法。 规则匹配部分,Rete算法是关键,但用户可能对算法细节不太熟悉,需要简单解释其工作原理,同时提到其他算法如线性匹配,说明它们的优缺点。比如,Rete适合复杂规则但占用内存多,线性匹配适合简单场景但效率可能不高。 冲突解决策略方面,用户可能需要知道不同的优先级处理方式,比如优先级排序、最近使用规则等,这有助于他们在设计时选择合适的策略。结果处理模块需要强调外部系统的集成,比如如何触发业务逻辑或记录日志。 性能优化方面,用户可能关心实际应用中的瓶颈,所以需要提到算法选择、并行处理和缓存机制。扩展性方面,分布式和动态加载规则是常见需求,特别是微服务架构下,动态加载可以避免重启服务。 设计原则部分,解耦、可维护性和安全性都是关键点,用户可能希望确保他们的规则引擎灵活且安全,避免规则注入攻击。最后,通过一个简单的示例流程,帮助用户更好地理解整个设计过程。 需要确保使用正确的中文术语,避免技术术语翻译错误,同时保持口语化,但不过于随意。检查是否有遗漏的重要部分,比如是否提到测试和验证,或者在规则存储部分是否需要详细说明数据库或文件系统的选择。 可能用户的实际应用场景是什么?比如是高并发的电商系统,还是需要复杂决策的金融系统?不同的场景可能影响设计选择,但作为一般性介绍,保持广泛适用性比较好。 最后,是否需要推荐进一步的学习资源,比如相关论文或开源项目代码库?不过用户之前已经问过开源项目推荐,这里可能更关注设计本身,所以保持回答聚焦在设计要点上。</think>规则引擎的设计是一个涉及**规则表达、匹配算法、执行效率**等多方面考量的复杂过程。以下是规则引擎设计的核心模块关键设计思路: --- ### 一、核心设计模块 1. **规则定义模块** - **规则语法设计**:需支持灵活的表达方式,例如: - **声明式规则**(如 Drools 的 DRL 语言): ```drl rule "成年人折扣规则" when $user: User(age >= 18) then $user.setDiscount(0.1); end ``` - **脚本式规则**(如 JavaScript/Python 脚本) - **表格驱动规则**(如 Excel 表格解析)。 - **规则存储**:支持数据库、文件系统或内存存储,需考虑版本控制热更新能力。 2. **规则匹配模块** - **核心算法选择**: - **Rete 算法**:通过构建规则网络缓存中间结果,适合复杂规则但内存消耗较高。 - **线性匹配**:逐条遍历规则,简单但性能较低。 - **Phreak 算法**(Drools 优化版):结合 Rete 和流式处理,提升大规模规则集性能。 3. **规则执行模块** - **冲突解决策略**:当多条规则被触发时,需定义优先级策略: - 按规则优先级排序(如 `salience` 属性)。 - 按规则最近使用时间(LRU)。 - **结果处理**:支持回写数据、触发事件或调用外部服务。 --- ### 二、性能优化关键 1. **算法优化** - 使用 Rete/Phreak 算法减少重复计算。 - 对规则条件进行**索引化**(如哈希表加速条件匹配)。 2. **并行处理** - 将规则集拆分为独立子集并行执行(需避免规则间依赖)。 3. **缓存机制** - 缓存频繁使用的规则匹配结果(如基于事实数据的哈希值)。 --- ### 三、扩展性设计 1. **分布式规则引擎** - 通过分片(Sharding)将规则集分布到多个节点,适用于高并发场景(如金融风控系统)。 2. **动态规则加载** - 支持运行时新增/修改规则(如通过 API 或配置中心推送)。 --- ### 四、设计原则 1. **解耦性** - 规则业务代码分离,通过 DSL(领域特定语言)或配置文件定义规则。 2. **可维护性** - 提供规则调试工具(如日志追踪规则触发路径)。 3. **安全性** - 防止规则注入攻击(如脚本引擎的沙箱隔离)。 --- ### 五、示例设计流程 1. 用户提交订单 → 引擎读取用户属性(年龄、会员等级等)。 2. 匹配所有符合条件的促销规则(如“满减”“会员折扣”)。 3. 按优先级执行规则,计算最终价格。 4. 记录执行日志并返回结果。 --- ### 六、开源参考实现 - **Drools**:学习其 Rete 网络实现规则冲突解决策略。 - **EasyRules**:研究轻量级规则引擎的事件驱动模型。 规则引擎设计的难点在于**平衡灵活性性能**,需根据实际场景选择算法架构模式。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值