commit b0d224dc647e7f2629d40e0037eb239603b66355 Author: Snowykami Date: Sat Nov 1 13:20:02 2025 +0800 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. diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..fdce5b3 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e6905a2 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env* \ No newline at end of file diff --git a/cmd/db/main.go b/cmd/db/main.go new file mode 100644 index 0000000..449a98e --- /dev/null +++ b/cmd/db/main.go @@ -0,0 +1,14 @@ +package main + +import ( + "fmt" + + "git.liteyuki.org/LiteyukiStudio/folium/dbx" +) + +func main() { + // 初始化数据库连接 + db := dbx.GetDB(nil, "test") + + fmt.Println(db) +} diff --git a/cmd/log/main.go b/cmd/log/main.go new file mode 100644 index 0000000..c7e224f --- /dev/null +++ b/cmd/log/main.go @@ -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") +} diff --git a/dbx/init.go b/dbx/init.go new file mode 100644 index 0000000..0e69954 --- /dev/null +++ b/dbx/init.go @@ -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)) + } +} diff --git a/dbx/mysql.go b/dbx/mysql.go new file mode 100644 index 0000000..241796d --- /dev/null +++ b/dbx/mysql.go @@ -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 + "`" +} diff --git a/dbx/postgres.go b/dbx/postgres.go new file mode 100644 index 0000000..1177680 --- /dev/null +++ b/dbx/postgres.go @@ -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 DB(postgres) + 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 + `"` +} diff --git a/dbx/sqlite.go b/dbx/sqlite.go new file mode 100644 index 0000000..356d123 --- /dev/null +++ b/dbx/sqlite.go @@ -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{}, + ) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..eafa50b --- /dev/null +++ b/go.mod @@ -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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..4d9987a --- /dev/null +++ b/go.sum @@ -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= diff --git a/hertzx/hz.go b/hertzx/hz.go new file mode 100644 index 0000000..3da0521 --- /dev/null +++ b/hertzx/hz.go @@ -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...) +} diff --git a/logx/collector.go b/logx/collector.go new file mode 100644 index 0000000..11aaae2 --- /dev/null +++ b/logx/collector.go @@ -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 collector(Fluent/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 +} diff --git a/logx/log.go b/logx/log.go new file mode 100644 index 0000000..b3e70db --- /dev/null +++ b/logx/log.go @@ -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) +} diff --git a/osx/osx.go b/osx/osx.go new file mode 100644 index 0000000..423d12b --- /dev/null +++ b/osx/osx.go @@ -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) + } +}