new-api 代码学习
new-api:新一代大模型网关与AI资产管理系统.
程序入口
程序入口:main.go
embed 指令
//go:embed web/dist
var buildFS embed.FS
embed.FS 是一个文件系统,用来嵌入整个目录。
这段代码表示将当前目录下的 web/dist
目录及其所有内容嵌入最终二进制文件。
//go:embed web/dist/index.html
var indexPage []byte
这里直接嵌入单个 HTML 文件到字节切片中,适合快速访问单个文件,比如作为模板加载或者直接返回给客户端。
读取配置文件
err := godotenv.Load(".env")
if err != nil {
common.SysLog("Support for .env file is disabled")
}
godotenv.Load(".env")
用于加载 .env
文件,将配置读取到环境变量中(通过 os.Getenv("xxx")
来获取)。
命令行参数读取
common/init.go
使用到的包。 flag
、log
、os
、path/filepath
.
- flag 用于解析命令行参数。
- log 记录日志
- os 读取环境变量和读取文件
- path/filepath 跨平台文件路径处理
flag 解析命令行参数
flag.Int
、flag.String
、flag.Bool
返回值是一个指针类型,在使用时需要解引用(在变量前面加上*
)。
- flag.Int("port",3000,"the listening port")
- flag.Bool("version", false, "print version and exit")
- flag.String("log-dir", "./logs", "specify the log directory")
os 读取环境变量
ss := os.Getenv("SESSION_SECRET")
os 操作文件
- *os.Stat(LogDir) 获取文件信息
- os.IsNotExist(err) 判断文件是否存在
- *os.Mkdir(LogDir, 0777) 创建文件夹,权限为 0777
if _, err := os.Stat(*LogDir); os.IsNotExist(err) {
err = os.Mkdir(*LogDir, 0777) // 文件不存在时,创建文件夹,权限为 0777
if err != nil {
log.Fatal(err)
}
}
path/filepath 获取绝对路径
// 通过命令行获取到 log-dir 参数
LogDir = flag.String("log-dir", "./logs", "specify the log directory")
// 获取到 logdir 的绝对路径
*LogDir, err = filepath.Abs(*LogDir)
源码
package common
import (
"flag" // 处理命令行参数
"fmt" // 格式化输入输出
"log" // 记录日志
"os" // 读取环境变量、文件操作等
"path/filepath" // 跨平台文件路径处理
)
var (
Port = flag.Int("port", 3000, "the listening port")
PrintVersion = flag.Bool("version", false, "print version and exit")
PrintHelp = flag.Bool("help", false, "print help and exit")
LogDir = flag.String("log-dir", "./logs", "specify the log directory")
)
func printHelp() {
fmt.Println("New API " + Version + " - All in one API service for OpenAI API.")
fmt.Println("Copyright (C) 2023 JustSong. All rights reserved.")
fmt.Println("GitHub: https://github.com/songquanpeng/one-api")
fmt.Println("Usage: one-api [--port <port>] [--log-dir <log directory>] [--version] [--help]")
}
func LoadEnv() {
flag.Parse() // 解析命令行参数,赋值给全局变量
if *PrintVersion {
fmt.Println(Version)
os.Exit(0)
}
if *PrintHelp {
printHelp()
os.Exit(0)
}
if os.Getenv("SESSION_SECRET") != "" {
ss := os.Getenv("SESSION_SECRET")
if ss == "random_string" {
log.Println("WARNING: SESSION_SECRET is set to the default value 'random_string', please change it to a random string.")
log.Println("警告:SESSION_SECRET被设置为默认值'random_string',请修改为随机字符串。")
log.Fatal("Please set SESSION_SECRET to a random string.")
} else {
SessionSecret = ss
}
}
// delete something....
if *LogDir != "" {
var err error
*LogDir, err = filepath.Abs(*LogDir) // 获取 LogDir 的绝对路径
if err != nil {
log.Fatal(err) // log.Fatal() 会打印错误信息并退出程序
}
if _, err := os.Stat(*LogDir); os.IsNotExist(err) {
err = os.Mkdir(*LogDir, 0777) // 文件不存在时,创建文件夹,权限为 0777
if err != nil {
log.Fatal(err)
}
}
}
}
日志记录
核心:一个基于gin的日志系统,支持日志文件切换、并发安全、不同级别的日志记录,以及配额格式化等功能。但需要注意潜在的竞态条件和资源管理问题。
const (
loggerINFO = "INFO"
loggerWarn = "WARN"
loggerError = "ERR"
)
// 日志文件最大行数
const maxLogCount = 1000000
// 计数器
var logCount int
// 互斥锁,并发控制,防止多个goroutine同时操作日志文件。
// 保证日志初始化操作的原子性。
var setupLogLock sync.Mutex
var setupLogWorking bool
func SetupLogger() {
if *LogDir != "" {
ok := setupLogLock.TryLock()
if !ok {
// 获取失败表示已经有 goroutine 在处理。
log.Println("setup log is already working")
return
}
// 在函数退出时释放锁
defer func() {
setupLogLock.Unlock()
setupLogWorking = false
}()
// 日志文件
logPath := filepath.Join(*LogDir, fmt.Sprintf("oneapi-%s.log", time.Now().Format("20060102150405")))
// 打开文件, 权限:追加、创建、只写
fd, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal("failed to open log file")
}
// 将gin的默认写和错误写设置为同时输出到标准输出和日志文件。这说明该日志系统同时支持控制台和文件输出。
gin.DefaultWriter = io.MultiWriter(os.Stdout, fd)
gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd)
}
}
func logHelper(ctx context.Context, level string, msg string) {
writer := gin.DefaultErrorWriter
if level == loggerINFO {
writer = gin.DefaultWriter
}
// 获取请求ID, 从请求中获取 X-Oneapi-Request-Id 请求头
id := ctx.Value(RequestIdKey)
now := time.Now()
_, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg)
logCount++ // we don't need accurate count, so no lock here
if logCount > maxLogCount && !setupLogWorking {
logCount = 0
setupLogWorking = true
// 通过gopool.Go启动一个goroutine调用SetupLogger
gopool.Go(func() {
SetupLogger()
})
}
}
Gin release mode
Gin框架有不同的模式,比如debug和release.
- debug(默认模式): 有更多的日志输出(会打印路由信息、错误详情)。 开发环境使用,以便获得更详细的调试信息,比如请求的日志、错误信息等。
- release: 可能更注重性能,减少日志输出。生产环境使用
- test: 用于测试。测试环境使用
gorm 数据库操作
连接到数据库
https://gorm.io/zh_CN/docs/connecting_to_the_database.html
db, err := gorm.Open(mysql.New(mysql.Config{
DSN: "gorm:gorm@tcp(127.0.0.1:3306)/gorm?charset=utf8&parseTime=True&loc=Local", // DSN data source name
DefaultStringSize: 256, // string 类型字段的默认长度
DisableDatetimePrecision: true, // 禁用 datetime 精度,MySQL 5.6 之前的数据库不支持
DontSupportRenameIndex: true, // 重命名索引时采用删除并新建的方式,MySQL 5.7 之前的数据库和 MariaDB 不支持重命名索引
DontSupportRenameColumn: true, // 用 `change` 重命名列,MySQL 8 之前的数据库和 MariaDB 不支持重命名列
SkipInitializeWithVersion: false, // 根据当前 MySQL 版本自动配置
}), &gorm.Config{})
func chooseDB(envName string) (*gorm.DB, error) {
defer func() {
initCol()
}()
dsn := os.Getenv(envName)
if dsn != "" {
// use pg 数据库
if strings.HasPrefix(dsn, "postgres://") {
// Use PostgreSQL
common.SysLog("using PostgreSQL as database")
common.UsingPostgreSQL = true
return gorm.Open(postgres.New(postgres.Config{
DSN: dsn,
PreferSimpleProtocol: true, // disables implicit prepared statement usage
}), &gorm.Config{
PrepareStmt: true, // precompile SQL
})
}
// use SQLite
if strings.HasPrefix(dsn, "local") {
common.SysLog("SQL_DSN not set, using SQLite as database")
common.UsingSQLite = true
return useSQLite()
}
// Use MySQL
common.SysLog("using MySQL as database")
// check parseTime
if !strings.Contains(dsn, "parseTime") {
if strings.Contains(dsn, "?") {
dsn += "&parseTime=true"
} else {
dsn += "?parseTime=true"
}
}
common.UsingMySQL = true
return gorm.Open(mysql.Open(dsn), &gorm.Config{
PrepareStmt: true, // precompile SQL
})
}
// Use SQLite
common.SysLog("SQL_DSN not set, using SQLite as database")
common.UsingSQLite = true
return useSQLite()
}
// 使用 SQLite 数据库
func useSQLite() (*gorm.DB, error) {
return gorm.Open(sqlite.Open(common.SQLitePath), &gorm.Config{
PrepareStmt: true, // precompile SQL
})
}