feat: add logging and collector implementations

- Introduced `hertzx` package with `NewHertz` function for server initialization.
- Implemented `logx` package with various log collectors: Console, Loki, Elasticsearch, and Prometheus.
- Added `Logger` struct to manage logging levels and collectors.
- Created environment variable loading functionality in `osx` package to support configuration.
- Enhanced logging capabilities with structured log entries and asynchronous collection.
This commit is contained in:
2025-11-01 13:20:02 +08:00
commit b0d224dc64
14 changed files with 1275 additions and 0 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
.env*

14
cmd/db/main.go Normal file
View File

@@ -0,0 +1,14 @@
package main
import (
"fmt"
"git.liteyuki.org/LiteyukiStudio/folium/dbx"
)
func main() {
// 初始化数据库连接
db := dbx.GetDB(nil, "test")
fmt.Println(db)
}

13
cmd/log/main.go Normal file
View File

@@ -0,0 +1,13 @@
package main
import (
"git.liteyuki.org/LiteyukiStudio/folium/logx"
)
func main() {
// 初始化日志
logx.Debug("This is a debug log")
logx.Info("This is an info log")
logx.Warn("This is a warning log")
logx.Error("This is an error log")
}

88
dbx/init.go Normal file
View File

@@ -0,0 +1,88 @@
package dbx
import (
"git.liteyuki.org/LiteyukiStudio/folium/osx"
"gorm.io/gorm"
)
type DBDriverType string
const (
Postgres DBDriverType = "postgres"
MySQL DBDriverType = "mysql"
SQLite DBDriverType = "sqlite"
)
// DBConfig 持有数据库连接配置,可以部分填写,未填写的字段会从环境变量读取默认值
type DBConfig struct {
Driver DBDriverType
Host string
Port string
User string
Password string
Sslmode string
}
// NewDBConfigFromEnv 从环境变量构造默认配置
func NewDBConfigFromEnv() *DBConfig {
return &DBConfig{
Driver: DBDriverType(osx.GetEnv("DB_DRIVER", string(Postgres))),
Host: osx.GetEnv("DB_HOST", "localhost"),
Port: osx.GetEnv("DB_PORT", "5432"),
User: osx.GetEnv("DB_USER", "user"),
Password: osx.GetEnv("DB_PASSWORD", "password"),
Sslmode: osx.GetEnv("DB_SSLMODE", "disable"),
}
}
// FillDefaultsFromEnv 对于为空的字段,用环境变量或默认值补全
func (c *DBConfig) FillDefaultsFromEnv() {
if c == nil {
c = &DBConfig{}
}
if c.Driver == "" {
c.Driver = DBDriverType(osx.GetEnv("DB_DRIVER", string(Postgres)))
}
if c.Host == "" {
c.Host = osx.GetEnv("DB_HOST", "localhost")
}
if c.Port == "" {
c.Port = osx.GetEnv("DB_PORT", "5432")
}
if c.User == "" {
c.User = osx.GetEnv("DB_USER", "user")
}
if c.Password == "" {
c.Password = osx.GetEnv("DB_PASSWORD", "password")
}
if c.Sslmode == "" {
c.Sslmode = osx.GetEnv("DB_SSLMODE", "disable")
}
}
// GetDB 使用给定配置(或 DefaultDBConfig if nil并返回对应的 *gorm.DB
// 如果 dbName 为空则使用配置中的 Name 字段(仍会从环境变量补全)
func GetDB(cfg *DBConfig, dbName string) *gorm.DB {
if cfg == nil {
cfg = NewDBConfigFromEnv()
}
cfg.FillDefaultsFromEnv()
if dbName == "" {
dbName = osx.GetEnv("DB_NAME", "database")
if dbName == "" {
panic("database name is not specified")
}
}
switch cfg.Driver {
case Postgres:
return GetPostgresDB(cfg, dbName)
case MySQL:
return GetMySQLDB(cfg, dbName)
case SQLite:
return GetSQLiteDB(cfg, dbName)
default:
panic("unsupported database driver: " + string(cfg.Driver))
}
}

71
dbx/mysql.go Normal file
View File

@@ -0,0 +1,71 @@
package dbx
import (
"context"
"fmt"
"strings"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
)
func GetMySQLDB(cfg *DBConfig, dbName string) *gorm.DB {
db, err := getMySQLInstance(cfg, dbName)
if err != nil {
panic("failed to connect to MySQL database: " + err.Error())
}
return db
}
func getMySQLInstance(cfg *DBConfig, dbName string) (*gorm.DB, error) {
targetDSN := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
cfg.User, cfg.Password, cfg.Host, cfg.Port, dbName)
// 先尝试直接连接目标库
if db, err := gorm.Open(mysql.Open(targetDSN), &gorm.Config{}); err == nil {
return db, nil
}
// 以不指定数据库的 DSN 连接(用于创建数据库)
adminDSN := fmt.Sprintf("%s:%s@tcp(%s:%s)/?charset=utf8mb4&parseTime=True&loc=Local",
cfg.User, cfg.Password, cfg.Host, cfg.Port)
adminDB, err := gorm.Open(mysql.Open(adminDSN), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("connect admin mysql failed: %w", err)
}
// 关闭底层连接
if sqlDB, e := adminDB.DB(); e == nil {
defer sqlDB.Close()
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 检查是否存在(使用 INFORMATION_SCHEMA
var count int64
checkSQL := "SELECT count(*) FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?"
if err := adminDB.WithContext(ctx).Raw(checkSQL, dbName).Scan(&count).Error; err != nil {
return nil, fmt.Errorf("check mysql database existence failed: %w", err)
}
if count == 0 {
ident := escapeMySQLIdentifier(dbName)
createSQL := fmt.Sprintf("CREATE DATABASE %s DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci", ident)
if err := adminDB.WithContext(ctx).Exec(createSQL).Error; err != nil {
return nil, fmt.Errorf("create mysql database %s failed: %w", dbName, err)
}
}
// 重试连接目标数据库
db2, err := gorm.Open(mysql.Open(targetDSN), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("connect mysql target after create failed: %w", err)
}
return db2, nil
}
// 辅助:安全转义 MySQL 标识符(用反引号并把 ` 替换为 “)
func escapeMySQLIdentifier(s string) string {
s = strings.ReplaceAll(s, "`", "``")
return "`" + s + "`"
}

70
dbx/postgres.go Normal file
View File

@@ -0,0 +1,70 @@
package dbx
import (
"context"
"fmt"
"strings"
"time"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
func GetPostgresDB(cfg *DBConfig, dbName string) *gorm.DB {
db, err := getPostgresInstance(cfg, dbName)
if err != nil {
panic("failed to connect to Postgres database: " + err.Error())
}
return db
}
func getPostgresInstance(cfg *DBConfig, dbName string) (*gorm.DB, error) {
targetDSN := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s",
cfg.Host, cfg.Port, cfg.User, cfg.Password, dbName, cfg.Sslmode)
// 先尝试直接连接目标库
if db, err := gorm.Open(postgres.Open(targetDSN), &gorm.Config{}); err == nil {
return db, nil
}
// 连接 admin DBpostgres
adminDSN := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=postgres sslmode=%s",
cfg.Host, cfg.Port, cfg.User, cfg.Password, cfg.Sslmode)
adminDB, err := gorm.Open(postgres.Open(adminDSN), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("connect admin postgres failed: %w", err)
}
// 确保关闭底层连接
if sqlDB, e := adminDB.DB(); e == nil {
defer sqlDB.Close()
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// 检查是否存在
var count int64
if err := adminDB.WithContext(ctx).Raw("SELECT count(*) FROM pg_database WHERE datname = ?", dbName).Scan(&count).Error; err != nil {
return nil, fmt.Errorf("check database existence failed: %w", err)
}
if count == 0 {
ident := escapePostgresIdentifier(dbName)
createSQL := fmt.Sprintf("CREATE DATABASE %s", ident)
if err := adminDB.WithContext(ctx).Exec(createSQL).Error; err != nil {
return nil, fmt.Errorf("create database %s failed: %w", dbName, err)
}
}
// 尝试连接目标数据库
db2, err := gorm.Open(postgres.Open(targetDSN), &gorm.Config{})
if err != nil {
return nil, fmt.Errorf("connect target after create failed: %w", err)
}
return db2, nil
}
// 辅助:安全转义 Postgres 标识符(用双引号并把 " 替换为 ""
func escapePostgresIdentifier(s string) string {
s = strings.ReplaceAll(s, `"`, `""`)
return `"` + s + `"`
}

22
dbx/sqlite.go Normal file
View File

@@ -0,0 +1,22 @@
package dbx
import (
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
func GetSQLiteDB(cfg *DBConfig, dbName string) *gorm.DB {
db, err := getSQLiteInstance(cfg, dbName)
if err != nil {
panic("failed to connect to SQLite database: " + err.Error())
}
return db
}
func getSQLiteInstance(cfg *DBConfig, dbName string) (*gorm.DB, error) {
// 对 sqlite, dbName 可以是文件路径或 ':memory:'
return gorm.Open(
sqlite.Open(dbName),
&gorm.Config{},
)
}

67
go.mod Normal file
View File

@@ -0,0 +1,67 @@
module git.liteyuki.org/LiteyukiStudio/folium
go 1.23.3
require (
github.com/prometheus/client_golang v1.23.2
go.uber.org/zap v1.27.0
gorm.io/gorm v1.31.0
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/bytedance/gopkg v0.1.1 // indirect
github.com/bytedance/sonic v1.14.0 // indirect
github.com/bytedance/sonic/loader v0.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cloudwego/base64x v0.1.5 // indirect
github.com/cloudwego/gopkg v0.1.4 // indirect
github.com/cloudwego/netpoll v0.7.0 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/glebarez/go-sqlite v1.21.2 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/golang/protobuf v1.5.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.6.0 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/joho/godotenv v1.5.1 // indirect
github.com/klauspost/cpuid/v2 v2.2.3 // indirect
github.com/mattn/go-isatty v0.0.17 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nyaruka/phonenumbers v1.0.55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rogpeppe/go-internal v1.13.1 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/multierr v1.10.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.35.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
modernc.org/libc v1.22.5 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.23.1 // indirect
)
require (
github.com/cloudwego/hertz v0.10.3
github.com/glebarez/sqlite v1.11.0
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/sirupsen/logrus v1.9.3
golang.org/x/text v0.28.0 // indirect
gorm.io/driver/mysql v1.6.0
gorm.io/driver/postgres v1.6.0
)

208
go.sum Normal file
View File

@@ -0,0 +1,208 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bytedance/gopkg v0.1.1 h1:3azzgSkiaw79u24a+w9arfH8OfnQQ4MHUt9lJFREEaE=
github.com/bytedance/gopkg v0.1.1/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/cloudwego/gopkg v0.1.4 h1:EoQiCG4sTonTPHxOGE0VlQs+sQR+Hsi2uN0qqwu8O50=
github.com/cloudwego/gopkg v0.1.4/go.mod h1:FQuXsRWRsSqJLsMVd5SYzp8/Z1y5gXKnVvRrWUOsCMI=
github.com/cloudwego/hertz v0.10.3 h1:NFcQAjouVJsod79XPLC/PaFfHgjMTYbiErmW+vGBi8A=
github.com/cloudwego/hertz v0.10.3/go.mod h1:W5dUFXZPZkyfjMMo3EQrMQbofuvTsctM9IxmhbkuT18=
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
github.com/cloudwego/netpoll v0.7.0 h1:bDrxQaNfijRI1zyGgXHQoE/nYegL0nr+ijO1Norelc4=
github.com/cloudwego/netpoll v0.7.0/go.mod h1:PI+YrmyS7cIr0+SD4seJz3Eo3ckkXdu2ZVKBLhURLNU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI=
github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU=
github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo=
github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.3 h1:sxCkb+qR91z4vsqw4vGGZlDgPz3G7gjaLyK3V8y70BU=
github.com/klauspost/cpuid/v2 v2.2.3/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-isatty v0.0.17 h1:BTarxUcIeDqL27Mc+vyvdWYSL28zpIhv3RoTdsLMPng=
github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nyaruka/phonenumbers v1.0.55 h1:bj0nTO88Y68KeUQ/n3Lo2KgK7lM1hF7L9NFuwcCl3yg=
github.com/nyaruka/phonenumbers v1.0.55/go.mod h1:sDaTZ/KPX5f8qyV9qN+hIm+4ZBARJrupC6LuhshJq1U=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE=
modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY=
modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ=
modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E=
modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds=
modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM=
modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=

14
hertzx/hz.go Normal file
View File

@@ -0,0 +1,14 @@
package hertzx
import (
"git.liteyuki.org/LiteyukiStudio/folium/osx"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/common/config"
)
func NewHertz(opts ...config.Option) *server.Hertz {
opts = append([]config.Option{
server.WithHostPorts(osx.GetEnv("HERTZ_HOST_PORT", ":8888")),
}, opts...)
return server.New(opts...)
}

187
logx/collector.go Normal file
View File

@@ -0,0 +1,187 @@
package logx
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
"git.liteyuki.org/LiteyukiStudio/folium/osx"
"github.com/prometheus/client_golang/prometheus"
)
type ConsoleCollector struct{}
func NewConsoleCollector() *ConsoleCollector { return &ConsoleCollector{} }
func (c *ConsoleCollector) Collect(ctx context.Context, e Entry) error {
// 简单输出 JSON 行,供外部 log collectorFluent/FluentBit抓取
b, _ := json.Marshal(e)
fmt.Fprintln(os.Stdout, string(b))
return nil
}
func (c *ConsoleCollector) Close() error { return nil }
// LokiCollector把日志推送到 Loki push API基本实现
type LokiCollector struct {
url string
client *http.Client
labels string // e.g. `{job="myapp"}`
}
func NewLokiCollector(collectorURL string, labels map[string]string) *LokiCollector {
// 构建 labels 字符串
lbl := "{"
first := true
for k, v := range labels {
if !first {
lbl += ","
}
lbl += fmt.Sprintf("%s=\"%s\"", k, v)
first = false
}
lbl += "}"
return &LokiCollector{
url: collectorURL,
client: &http.Client{Timeout: 5 * time.Second},
labels: lbl,
}
}
type lokiStream struct {
Stream map[string]string `json:"stream"`
Values [][2]string `json:"values"`
}
type lokiPush struct {
Streams []lokiStream `json:"streams"`
}
func (c *LokiCollector) Collect(ctx context.Context, e Entry) error {
// Loki requires timestamp in nanoseconds as string
ts := fmt.Sprintf("%d", e.Time.UnixNano())
lineB, _ := json.Marshal(e)
stream := lokiStream{
Stream: map[string]string{"level": string(e.Level)},
Values: [][2]string{{ts, string(lineB)}},
}
push := lokiPush{Streams: []lokiStream{stream}}
body, _ := json.Marshal(push)
req, _ := http.NewRequestWithContext(ctx, "POST", c.url+"/loki/api/v1/push", bytes.NewBuffer(body))
req.Header.Set("Content-Type", "application/json")
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("loki push status %d", resp.StatusCode)
}
return nil
}
func (c *LokiCollector) Close() error { return nil }
// ESCollector把日志写入 Elasticsearch 简单实现(使用 index API
type ESCollector struct {
url string // e.g. http://es:9200
index string // index name
client *http.Client
auth *basicAuth // optional
}
type basicAuth struct {
user string
pass string
}
func NewESCollector(url, index string, authUser, authPass string) *ESCollector {
var auth *basicAuth
if authUser != "" {
auth = &basicAuth{user: authUser, pass: authPass}
}
return &ESCollector{
url: url,
index: index,
client: &http.Client{Timeout: 5 * time.Second},
auth: auth,
}
}
func (c *ESCollector) Collect(ctx context.Context, e Entry) error {
b, _ := json.Marshal(e)
endpoint := fmt.Sprintf("%s/%s/_doc", c.url, c.index)
req, _ := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(b))
req.Header.Set("Content-Type", "application/json")
if c.auth != nil {
req.SetBasicAuth(c.auth.user, c.auth.pass)
}
resp, err := c.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode >= 300 {
return fmt.Errorf("es index status %d", resp.StatusCode)
}
return nil
}
func (c *ESCollector) Close() error { return nil }
// PromCollector记录日志计数到 Prometheus便于监控日志量
type PromCollector struct {
counter *prometheus.CounterVec
}
func NewPromCollector(namespace string) *PromCollector {
cv := prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Name: "app_logs_total",
Help: "Total application logs by level",
}, []string{"level"})
// 忽略重复注册错误(如果已存在,使用已注册的)
_ = prometheus.Register(cv)
return &PromCollector{
counter: cv,
}
}
func (p *PromCollector) Collect(ctx context.Context, e Entry) error {
p.counter.WithLabelValues(string(e.Level)).Inc()
return nil
}
func (p *PromCollector) Close() error { return nil }
// LoadCollectorsFromEnv 根据环境变量 LOG_COLLECTORS 加载对应的 collectors
func LoadCollectorsFromEnv() []Collector {
collectors := osx.GetEnv("LOG_COLLECTORS", "")
var result []Collector
if collectors == "" {
return result
}
types := bytes.Split([]byte(collectors), []byte(","))
for _, t := range types {
switch string(bytes.TrimSpace(t)) {
case "console":
result = append(result, NewConsoleCollector())
case "loki":
lokiURL := osx.GetEnv("LOKI_URL", "http://localhost:3100")
labels := map[string]string{
"job": osx.GetEnv("LOKI_JOB", "myapp"),
}
result = append(result, NewLokiCollector(lokiURL, labels))
case "elasticsearch":
esURL := osx.GetEnv("ES_URL", "http://localhost:9200")
esIndex := osx.GetEnv("ES_INDEX", "app-logs")
esUser := osx.GetEnv("ES_USER", "")
esPass := osx.GetEnv("ES_PASS", "")
result = append(result, NewESCollector(esURL, esIndex, esUser, esPass))
case "prometheus":
namespace := osx.GetEnv("PROM_NAMESPACE", "myapp")
result = append(result, NewPromCollector(namespace))
// 可扩展更多 collector 类型
}
}
return result
}

442
logx/log.go Normal file
View File

@@ -0,0 +1,442 @@
package logx
import (
"context"
"strings"
"sync"
"time"
"git.liteyuki.org/LiteyukiStudio/folium/osx"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// 支持的日志等级
type Level string
const (
LevelTrace Level = "trace"
LevelDebug Level = "debug"
LevelInfo Level = "info"
LevelWarn Level = "warn"
LevelError Level = "error"
LevelFatal Level = "fatal"
LevelPanic Level = "panic"
)
// Entry 表示一条日志的可传输结构
type Entry struct {
Level Level `json:"level"`
Time time.Time `json:"time"`
Msg string `json:"msg"`
Fields map[string]interface{} `json:"fields,omitempty"`
}
// Collector 是外部收集器接口(用户可实现)
type Collector interface {
Collect(ctx context.Context, e Entry) error
Close() error
}
// Logger 支持本地打印并把日志发送到已注册的 collectors
type Logger struct {
zap *zap.Logger
collectors []Collector
mu sync.RWMutex
wg sync.WaitGroup
minLevel Level // 低于此级别的日志将被忽略
}
// 全局单例
var (
Default *Logger
defaultOnce sync.Once
)
// buildLoggerFromEnv 基于环境构造 Logger不设置全局变量
func buildLoggerFromEnv() *Logger {
// 输出格式
output := strings.ToLower(osx.GetEnv("LOG_OUTPUT", "console"))
cfg := zap.NewProductionConfig()
if output == "json" {
cfg.Encoding = "json"
} else {
cfg.Encoding = "console"
}
cfg.EncoderConfig.TimeKey = "ts"
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
// 让 zap 本身也服从 LOG_LEVEL
levelStr := strings.ToLower(osx.GetEnv("LOG_LEVEL", osx.GetEnv("LOG_MIN_LEVEL", "info")))
var zapLevel zapcore.Level
switch levelStr {
case "trace", "debug":
zapLevel = zapcore.DebugLevel
case "info":
zapLevel = zapcore.InfoLevel
case "warn", "warning":
zapLevel = zapcore.WarnLevel
case "error":
zapLevel = zapcore.ErrorLevel
case "fatal":
zapLevel = zapcore.FatalLevel
case "panic":
zapLevel = zapcore.PanicLevel
default:
zapLevel = zapcore.InfoLevel
}
cfg.Level = zap.NewAtomicLevelAt(zapLevel)
// 可通过环境关闭 stacktrace 输出
if strings.ToLower(osx.GetEnv("LOG_DISABLE_STACKTRACE", "false")) == "true" {
cfg.DisableStacktrace = true
}
zl, _ := cfg.Build()
l := &Logger{
zap: zl,
collectors: nil,
minLevel: LevelInfo,
}
// 最低日志级别(同步 env
switch levelStr {
case "trace":
l.minLevel = LevelTrace
case "debug":
l.minLevel = LevelDebug
case "info":
l.minLevel = LevelInfo
case "warn", "warning":
l.minLevel = LevelWarn
case "error":
l.minLevel = LevelError
case "fatal":
l.minLevel = LevelFatal
case "panic":
l.minLevel = LevelPanic
default:
l.minLevel = LevelInfo
}
// 从环境加载并注册 collectors实现位于 collector.go
cols := LoadCollectorsFromEnv()
for _, c := range cols {
l.RegisterCollector(c)
}
return l
}
// InitLogger 初始化全局单例(只第一次生效),返回单例
func InitLogger() *Logger {
defaultOnce.Do(func() {
Default = buildLoggerFromEnv()
})
return Default
}
// GetLogger 返回全局单例,如果尚未初始化则会自动 InitLogger
func GetLogger() *Logger {
return InitLogger()
}
// 删除旧的 New / NewDefault保留 New(...) 风格的局部 logger 创建函数改名为 NewLocal可选
func NewLocal(zl *zap.Logger) *Logger {
if zl == nil {
// 使用一个轻量 zap 实例,不触碰全局单例
cfg := zap.NewProductionConfig()
cfg.Encoding = "console"
cfg.EncoderConfig.TimeKey = "ts"
cfg.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
zl2, _ := cfg.Build()
return &Logger{
zap: zl2,
minLevel: LevelDebug,
collectors: nil,
}
}
return &Logger{
zap: zl,
minLevel: LevelDebug,
collectors: nil,
}
}
// RegisterCollector 在运行时注册一个 Collector线程安全
func (l *Logger) RegisterCollector(c Collector) {
if c == nil {
return
}
l.mu.Lock()
defer l.mu.Unlock()
l.collectors = append(l.collectors, c)
}
// UnregisterCollector 注销已注册的 Collector按指针相等匹配
func (l *Logger) UnregisterCollector(c Collector) {
if c == nil {
return
}
l.mu.Lock()
defer l.mu.Unlock()
for i, col := range l.collectors {
if col == c {
l.collectors = append(l.collectors[:i], l.collectors[i+1:]...)
return
}
}
}
// ListCollectors 返回当前已注册的 collectors副本线程安全
func (l *Logger) ListCollectors() []Collector {
l.mu.RLock()
defer l.mu.RUnlock()
cp := make([]Collector, len(l.collectors))
copy(cp, l.collectors)
return cp
}
// Close 关闭所有 collectors阻塞直到异步发送结束
func (l *Logger) Close() error {
l.mu.RLock()
collectors := append([]Collector(nil), l.collectors...)
l.mu.RUnlock()
// 等待 async sending 完成
l.wg.Wait()
var firstErr error
for _, c := range collectors {
if err := c.Close(); err != nil && firstErr == nil {
firstErr = err
}
}
_ = l.zap.Sync()
return firstErr
}
// SetLevel 设置最低日志级别,低于该级别的日志将被忽略
func (l *Logger) SetLevel(level Level) {
l.mu.Lock()
defer l.mu.Unlock()
l.minLevel = level
}
// level 排序辅助
var levelOrder = map[Level]int{
LevelTrace: 0,
LevelDebug: 1,
LevelInfo: 2,
LevelWarn: 3,
LevelError: 4,
LevelFatal: 5,
LevelPanic: 6,
}
func (l *Logger) log(ctx context.Context, level Level, msg string, fields map[string]interface{}) {
// 级别过滤
l.mu.RLock()
min := l.minLevel
l.mu.RUnlock()
if levelOrder[level] < levelOrder[min] {
return
}
// 本地打印(保持原有行为),并尽量保持 trace/level 字段
switch level {
case LevelTrace:
l.zap.Debug(msg, zap.String("level", string(LevelTrace)), zap.Any("fields", fields))
case LevelDebug:
l.zap.Debug(msg, zap.Any("fields", fields))
case LevelInfo:
l.zap.Info(msg, zap.Any("fields", fields))
case LevelWarn:
l.zap.Warn(msg, zap.Any("fields", fields))
case LevelError:
l.zap.Error(msg, zap.Any("fields", fields))
case LevelFatal:
// Fatal 会调用 os.Exit(1)
l.zap.Fatal(msg, zap.Any("fields", fields))
case LevelPanic:
// Panic 会 panic
l.zap.Panic(msg, zap.Any("fields", fields))
default:
l.zap.Info(msg, zap.Any("level", level), zap.Any("fields", fields))
}
// 向 collectors 异步发送
l.mu.RLock()
collectors := append([]Collector(nil), l.collectors...)
l.mu.RUnlock()
if len(collectors) == 0 {
return
}
e := Entry{
Level: level,
Time: time.Now(),
Msg: msg,
Fields: fields,
}
for _, c := range collectors {
c := c
l.wg.Add(1)
go func() {
defer l.wg.Done()
ctx2, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
if err := c.Collect(ctx2, e); err != nil {
// collectors 的错误不影响主流程,记录到本地日志
l.zap.Warn("collector collect error", zap.Error(err))
}
}()
}
}
// 便捷方法(增加 Trace, Fatal, Panic
// 保留 Logger 的方法签名:需要 ctx
func (l *Logger) Trace(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelTrace, msg, fields)
}
func (l *Logger) Debug(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelDebug, msg, fields)
}
func (l *Logger) Info(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelInfo, msg, fields)
}
func (l *Logger) Warn(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelWarn, msg, fields)
}
func (l *Logger) Error(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelError, msg, fields)
}
func (l *Logger) Fatal(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelFatal, msg, fields)
}
func (l *Logger) Panic(ctx context.Context, msg string, fields map[string]interface{}) {
l.log(ctx, LevelPanic, msg, fields)
}
// SimpleLogger 提供不带 ctx 的简洁 API可选 fields
type SimpleLogger struct {
parent *Logger
}
func (s *SimpleLogger) Trace(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Trace(context.Background(), msg, f)
}
func (s *SimpleLogger) Debug(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Debug(context.Background(), msg, f)
}
func (s *SimpleLogger) Info(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Info(context.Background(), msg, f)
}
func (s *SimpleLogger) Warn(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Warn(context.Background(), msg, f)
}
func (s *SimpleLogger) Error(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Error(context.Background(), msg, f)
}
func (s *SimpleLogger) Fatal(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Fatal(context.Background(), msg, f)
}
func (s *SimpleLogger) Panic(msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
s.parent.Panic(context.Background(), msg, f)
}
// Simple 返回一个绑定到当前 Logger 的 SimpleLogger不创建新实例
func (l *Logger) Simple() *SimpleLogger {
return &SimpleLogger{parent: l}
}
// 包级简洁 API无需 ctx自动使用 background ctx调用者更推荐显式使用 Ctx* 以保留 trace
func Trace(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Trace(msg, fields...) }
func Debug(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Debug(msg, fields...) }
func Info(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Info(msg, fields...) }
func Warn(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Warn(msg, fields...) }
func Error(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Error(msg, fields...) }
func Fatal(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Fatal(msg, fields...) }
func Panic(msg string, fields ...map[string]interface{}) { GetLogger().Simple().Panic(msg, fields...) }
// 包级带 ctx 的 API当你有请求上下文包含 trace时使用
func CtxTrace(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Trace(ctx, msg, f)
}
func CtxDebug(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Debug(ctx, msg, f)
}
func CtxInfo(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Info(ctx, msg, f)
}
func CtxWarn(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Warn(ctx, msg, f)
}
func CtxError(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Error(ctx, msg, f)
}
func CtxFatal(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Fatal(ctx, msg, f)
}
func CtxPanic(ctx context.Context, msg string, fields ...map[string]interface{}) {
var f map[string]interface{}
if len(fields) > 0 {
f = fields[0]
}
GetLogger().Panic(ctx, msg, f)
}

78
osx/osx.go Normal file
View File

@@ -0,0 +1,78 @@
package osx
import (
"fmt"
"os"
"github.com/joho/godotenv"
)
func init() {
// 读取目录下的所有 .env 文件,加载环境变量
// 生产环境由于部署在kubernetes等平台通常会直接通过环境变量注入配置
// 因此这里的加载主要用于开发环境
loadDotEnvFiles()
}
func GetEnv(key string, defaultValue ...string) string {
if value, exists := os.LookupEnv(key); exists {
return value
}
if len(defaultValue) > 0 {
return defaultValue[0]
}
return ""
}
func GetEnvInt(key string, defaultValue int) int {
if valueStr, exists := os.LookupEnv(key); exists {
var value int
_, err := fmt.Sscanf(valueStr, "%d", &value)
if err == nil {
return value
}
}
return defaultValue
}
// GetEnvBool 支持 "true", "1", "True", "TRUE" 返回 true
// 支持 "false", "0", "False", "FALSE" 返回 false
// 如果环境变量不存在或无法解析,则返回 defaultValue
func GetEnvBool(key string, defaultValue bool) bool {
if valueStr, exists := os.LookupEnv(key); exists {
switch valueStr {
case "true", "1", "True", "TRUE":
return true
case "false", "0", "False", "FALSE":
return false
}
}
return defaultValue
}
func GetEnvT[T any](key string, parser func(string) (T, error), defaultValue T) T {
if valueStr, exists := os.LookupEnv(key); exists {
if value, err := parser(valueStr); err == nil {
return value
}
}
return defaultValue
}
func loadDotEnvFiles() {
dotEnvFiles := []string{".env.local", ".env", ".env.development", ".env.production"}
existedFiles := []string{}
for _, file := range dotEnvFiles {
if _, err := os.Stat(file); err == nil {
existedFiles = append(existedFiles, file)
}
}
if len(existedFiles) == 0 {
return
}
// 按顺序加载,后面的会覆盖前面的
err := godotenv.Load(existedFiles...)
if err != nil {
fmt.Printf("Error loading .env files: %v\n", err)
}
}