feat: 新增日志、配置文件和环境变量
This commit is contained in:
41
app.go
Normal file
41
app.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package go_web_gin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"git.hujye.com/infrastructure/go-web-gin/config"
|
||||||
|
"git.hujye.com/infrastructure/go-web-gin/server"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// App represents the application
|
||||||
|
type App struct {
|
||||||
|
server *server.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new App instance and loads config file
|
||||||
|
func New() *App {
|
||||||
|
// Load config file
|
||||||
|
if _, err := config.Load(); err != nil {
|
||||||
|
log.Printf("Warning: failed to load config file, using defaults: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &App{
|
||||||
|
server: server.New(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UseMiddleware registers global middleware
|
||||||
|
func (a *App) UseMiddleware(middleware ...gin.HandlerFunc) {
|
||||||
|
a.server.Engine().Use(middleware...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes registers routes with the given handler function
|
||||||
|
func (a *App) RegisterRoutes(registerFunc func(*gin.Engine)) {
|
||||||
|
registerFunc(a.server.Engine())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts the application server
|
||||||
|
func (a *App) Run(addr ...string) error {
|
||||||
|
return a.server.Run(addr...)
|
||||||
|
}
|
||||||
161
config/config.go
Normal file
161
config/config.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.hujye.com/infrastructure/go-web-gin/env"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
globalConfig *Config
|
||||||
|
viperInstance *viper.Viper
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config represents the application configuration
|
||||||
|
type Config struct {
|
||||||
|
Server ServerConfig `mapstructure:"server"`
|
||||||
|
App AppConfig `mapstructure:"app"`
|
||||||
|
Log LogConfig `mapstructure:"log"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServerConfig represents server configuration
|
||||||
|
type ServerConfig struct {
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Mode string `mapstructure:"mode"` // debug, release, test
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppConfig represents application configuration
|
||||||
|
type AppConfig struct {
|
||||||
|
Name string `mapstructure:"name"`
|
||||||
|
Environment string `mapstructure:"environment"`
|
||||||
|
LogLevel string `mapstructure:"log_level"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogConfig represents log configuration
|
||||||
|
type LogConfig struct {
|
||||||
|
OutputToFile bool `mapstructure:"output_to_file"`
|
||||||
|
Filename string `mapstructure:"filename"`
|
||||||
|
MaxSize int `mapstructure:"max_size"`
|
||||||
|
MaxBackups int `mapstructure:"max_backups"`
|
||||||
|
MaxAge int `mapstructure:"max_age"`
|
||||||
|
Compress bool `mapstructure:"compress"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads configuration from environment variable CFG_PATH
|
||||||
|
// Default path is config/config.yml
|
||||||
|
func Load() (*Config, error) {
|
||||||
|
v := viper.New()
|
||||||
|
v.SetConfigFile(env.GetCfgPath())
|
||||||
|
v.SetConfigType("yaml")
|
||||||
|
|
||||||
|
// Read environment variables
|
||||||
|
v.AutomaticEnv()
|
||||||
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||||
|
|
||||||
|
// Read config file
|
||||||
|
if err := v.ReadInConfig(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg Config
|
||||||
|
if err := v.Unmarshal(&cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
globalConfig = &cfg
|
||||||
|
viperInstance = v
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the global configuration
|
||||||
|
// Must call Load first
|
||||||
|
func Get() *Config {
|
||||||
|
if globalConfig == nil {
|
||||||
|
// Return default config if not loaded
|
||||||
|
return &Config{
|
||||||
|
Server: ServerConfig{
|
||||||
|
Port: 8080,
|
||||||
|
Host: "0.0.0.0",
|
||||||
|
Mode: "debug",
|
||||||
|
},
|
||||||
|
App: AppConfig{
|
||||||
|
Name: "go-web-gin",
|
||||||
|
Environment: string(env.Local),
|
||||||
|
LogLevel: "info",
|
||||||
|
},
|
||||||
|
Log: LogConfig{
|
||||||
|
OutputToFile: true,
|
||||||
|
Filename: "logs/app.log",
|
||||||
|
MaxSize: 100,
|
||||||
|
MaxBackups: 3,
|
||||||
|
MaxAge: 28,
|
||||||
|
Compress: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return globalConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAddr returns the server address (host:port)
|
||||||
|
func (c *Config) GetAddr() string {
|
||||||
|
return fmt.Sprintf("%s:%d", c.Server.Host, c.Server.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDebug returns true if server mode is debug
|
||||||
|
func (c *Config) IsDebug() bool {
|
||||||
|
return c.Server.Mode == "debug"
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRelease returns true if server mode is release
|
||||||
|
func (c *Config) IsRelease() bool {
|
||||||
|
return c.Server.Mode == "release"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetString returns a custom string config value by key
|
||||||
|
// Supports dot notation for nested keys, e.g. "database.host"
|
||||||
|
func GetString(key string) string {
|
||||||
|
if viperInstance != nil {
|
||||||
|
return viperInstance.GetString(key)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInt returns a custom int config value by key
|
||||||
|
func GetInt(key string) int {
|
||||||
|
if viperInstance != nil {
|
||||||
|
return viperInstance.GetInt(key)
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetBool returns a custom bool config value by key
|
||||||
|
func GetBool(key string) bool {
|
||||||
|
if viperInstance != nil {
|
||||||
|
return viperInstance.GetBool(key)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStringSlice returns a custom string slice config value by key
|
||||||
|
func GetStringSlice(key string) []string {
|
||||||
|
if viperInstance != nil {
|
||||||
|
return viperInstance.GetStringSlice(key)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetValue returns a custom config value of any type by key
|
||||||
|
func GetValue(key string) any {
|
||||||
|
if viperInstance != nil {
|
||||||
|
return viperInstance.Get(key)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Viper returns the underlying viper instance for advanced usage
|
||||||
|
func Viper() *viper.Viper {
|
||||||
|
return viperInstance
|
||||||
|
}
|
||||||
77
env/env.go
vendored
Normal file
77
env/env.go
vendored
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package env
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RunEnv represents the runtime environment
|
||||||
|
type RunEnv string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Local environment for local development
|
||||||
|
Local RunEnv = "LOCAL"
|
||||||
|
// Development environment for development/testing
|
||||||
|
Development RunEnv = "DEVELOPMENT"
|
||||||
|
// Production environment for production use
|
||||||
|
Production RunEnv = "PRODUCTION"
|
||||||
|
|
||||||
|
// RunEnvKey is the environment variable key for run environment
|
||||||
|
RunEnvKey = "RUN_ENV"
|
||||||
|
|
||||||
|
// CfgPathKey is the environment variable key for config file path
|
||||||
|
CfgPathKey = "CFG_PATH"
|
||||||
|
|
||||||
|
// DefaultCfgPath is the default config file path
|
||||||
|
DefaultCfgPath = "config/config.yml"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetRunEnv returns the current run environment from env variable
|
||||||
|
// Defaults to Local if not set or invalid
|
||||||
|
func GetRunEnv() RunEnv {
|
||||||
|
env := os.Getenv(RunEnvKey)
|
||||||
|
switch strings.ToUpper(env) {
|
||||||
|
case string(Development):
|
||||||
|
return Development
|
||||||
|
case string(Production):
|
||||||
|
return Production
|
||||||
|
default:
|
||||||
|
return Local
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the environment variable value for the given key
|
||||||
|
// Returns empty string if not set
|
||||||
|
func Get(key string) string {
|
||||||
|
return os.Getenv(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetWithDefault returns the environment variable value for the given key
|
||||||
|
// Returns defaultValue if not set
|
||||||
|
func GetWithDefault(key, defaultValue string) string {
|
||||||
|
if val := os.Getenv(key); val != "" {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsLocal returns true if current environment is Local
|
||||||
|
func IsLocal() bool {
|
||||||
|
return GetRunEnv() == Local
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDevelopment returns true if current environment is Development
|
||||||
|
func IsDevelopment() bool {
|
||||||
|
return GetRunEnv() == Development
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsProduction returns true if current environment is Production
|
||||||
|
func IsProduction() bool {
|
||||||
|
return GetRunEnv() == Production
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCfgPath returns the config file path from env variable
|
||||||
|
// Returns DefaultCfgPath if not set
|
||||||
|
func GetCfgPath() string {
|
||||||
|
return GetWithDefault(CfgPathKey, DefaultCfgPath)
|
||||||
|
}
|
||||||
56
go.mod
Normal file
56
go.mod
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
module git.hujye.com/infrastructure/go-web-gin
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-gonic/gin v1.10.0
|
||||||
|
github.com/spf13/viper v1.18.2
|
||||||
|
github.com/stretchr/testify v1.9.0
|
||||||
|
go.uber.org/zap v1.27.0
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/bytedance/sonic v1.11.6 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||||
|
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/magiconair/properties v1.8.7 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||||
|
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||||
|
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||||
|
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||||
|
github.com/spf13/afero v1.11.0 // indirect
|
||||||
|
github.com/spf13/cast v1.6.0 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/subosito/gotenv v1.6.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
|
golang.org/x/arch v0.8.0 // indirect
|
||||||
|
golang.org/x/crypto v0.23.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
|
golang.org/x/net v0.25.0 // indirect
|
||||||
|
golang.org/x/sys v0.20.0 // indirect
|
||||||
|
golang.org/x/text v0.15.0 // indirect
|
||||||
|
google.golang.org/protobuf v1.34.1 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
92
logger/encoder.go
Normal file
92
logger/encoder.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncoderType represents the log encoder type
|
||||||
|
type EncoderType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// JSONEncoder encodes logs as JSON
|
||||||
|
JSONEncoder EncoderType = "json"
|
||||||
|
// ConsoleEncoder encodes logs as human-readable text
|
||||||
|
ConsoleEncoder EncoderType = "console"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewEncoderConfig returns a default encoder config
|
||||||
|
func NewEncoderConfig() zapcore.EncoderConfig {
|
||||||
|
return zapcore.EncoderConfig{
|
||||||
|
TimeKey: "time",
|
||||||
|
LevelKey: "level",
|
||||||
|
NameKey: "logger",
|
||||||
|
CallerKey: "caller",
|
||||||
|
FunctionKey: zapcore.OmitKey,
|
||||||
|
MessageKey: "msg",
|
||||||
|
StacktraceKey: "stacktrace",
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: zapcore.LowercaseLevelEncoder,
|
||||||
|
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||||
|
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||||
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newConsoleEncoderConfig returns a colored encoder config for console output
|
||||||
|
func newConsoleEncoderConfig() zapcore.EncoderConfig {
|
||||||
|
return zapcore.EncoderConfig{
|
||||||
|
TimeKey: "time",
|
||||||
|
LevelKey: "level",
|
||||||
|
NameKey: "logger",
|
||||||
|
CallerKey: "caller",
|
||||||
|
FunctionKey: zapcore.OmitKey,
|
||||||
|
MessageKey: "msg",
|
||||||
|
StacktraceKey: "stacktrace",
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: CustomLevelEncoder, // 自定义带颜色的级别编码
|
||||||
|
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||||
|
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||||||
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewJSONEncoder creates a new JSON encoder
|
||||||
|
func NewJSONEncoder() zapcore.Encoder {
|
||||||
|
return zapcore.NewJSONEncoder(NewEncoderConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewConsoleEncoder creates a new console encoder with colors
|
||||||
|
func NewConsoleEncoder() zapcore.Encoder {
|
||||||
|
return zapcore.NewConsoleEncoder(newConsoleEncoderConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEncoder creates an encoder based on the given type
|
||||||
|
// Defaults to console encoder if type is invalid
|
||||||
|
func NewEncoder(encoderType EncoderType) zapcore.Encoder {
|
||||||
|
switch encoderType {
|
||||||
|
case JSONEncoder:
|
||||||
|
return NewJSONEncoder()
|
||||||
|
case ConsoleEncoder:
|
||||||
|
return NewConsoleEncoder()
|
||||||
|
default:
|
||||||
|
return NewConsoleEncoder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CustomLevelEncoder creates a custom level encoder with colors and styles
|
||||||
|
func CustomLevelEncoder(l zapcore.Level, enc zapcore.PrimitiveArrayEncoder) {
|
||||||
|
switch l {
|
||||||
|
case zapcore.DebugLevel:
|
||||||
|
enc.AppendString("\x1b[37m" + l.CapitalString() + "\x1b[0m") // 白色
|
||||||
|
case zapcore.InfoLevel:
|
||||||
|
enc.AppendString("\x1b[32m" + l.CapitalString() + "\x1b[0m") // 绿色
|
||||||
|
case zapcore.WarnLevel:
|
||||||
|
enc.AppendString("\x1b[33m" + l.CapitalString() + "\x1b[0m") // 黄色
|
||||||
|
case zapcore.ErrorLevel:
|
||||||
|
enc.AppendString("\x1b[31m" + l.CapitalString() + "\x1b[0m") // 红色
|
||||||
|
case zapcore.FatalLevel:
|
||||||
|
enc.AppendString("\x1b[35m" + l.CapitalString() + "\x1b[0m") // 紫色
|
||||||
|
default:
|
||||||
|
enc.AppendString(l.CapitalString())
|
||||||
|
}
|
||||||
|
}
|
||||||
180
logger/logger.go
Normal file
180
logger/logger.go
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.hujye.com/infrastructure/go-web-gin/config"
|
||||||
|
"git.hujye.com/infrastructure/go-web-gin/env"
|
||||||
|
"git.hujye.com/infrastructure/go-web-gin/web"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
"gopkg.in/natefinch/lumberjack.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
logger *zap.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
instance *Logger
|
||||||
|
once sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// parseLogLevel converts string log level to zapcore.Level
|
||||||
|
func parseLogLevel(level string) zapcore.Level {
|
||||||
|
switch strings.ToLower(level) {
|
||||||
|
case "debug":
|
||||||
|
return zapcore.DebugLevel
|
||||||
|
case "info":
|
||||||
|
return zapcore.InfoLevel
|
||||||
|
case "warn", "warning":
|
||||||
|
return zapcore.WarnLevel
|
||||||
|
case "error":
|
||||||
|
return zapcore.ErrorLevel
|
||||||
|
case "fatal":
|
||||||
|
return zapcore.FatalLevel
|
||||||
|
default:
|
||||||
|
return zapcore.InfoLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetLogger returns the singleton Logger instance
|
||||||
|
// Logger configuration is based on RUN_ENV environment variable and config file
|
||||||
|
func GetLogger() *Logger {
|
||||||
|
once.Do(func() {
|
||||||
|
runEnv := env.GetRunEnv()
|
||||||
|
cfg := config.Get()
|
||||||
|
|
||||||
|
// Production and Development use JSON encoder
|
||||||
|
// Local uses console encoder for better readability
|
||||||
|
var encoderType EncoderType
|
||||||
|
if runEnv == env.Production || runEnv == env.Development {
|
||||||
|
encoderType = JSONEncoder
|
||||||
|
} else {
|
||||||
|
encoderType = ConsoleEncoder
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create encoder
|
||||||
|
encoder := NewEncoder(encoderType)
|
||||||
|
|
||||||
|
// Create writer syncs (console + file if enabled)
|
||||||
|
var writerSyncs zapcore.WriteSyncer
|
||||||
|
if cfg.Log.OutputToFile {
|
||||||
|
// Create rotating file writer with lumberjack
|
||||||
|
fileWriter := &lumberjack.Logger{
|
||||||
|
Filename: cfg.Log.Filename,
|
||||||
|
MaxSize: cfg.Log.MaxSize,
|
||||||
|
MaxBackups: cfg.Log.MaxBackups,
|
||||||
|
MaxAge: cfg.Log.MaxAge,
|
||||||
|
Compress: cfg.Log.Compress,
|
||||||
|
}
|
||||||
|
// Write to both console and file
|
||||||
|
writerSyncs = zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stderr), zapcore.AddSync(fileWriter))
|
||||||
|
} else {
|
||||||
|
// Write to console only
|
||||||
|
writerSyncs = zapcore.AddSync(os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse log level from config
|
||||||
|
logLevel := parseLogLevel(cfg.App.LogLevel)
|
||||||
|
|
||||||
|
// Create core with custom encoder
|
||||||
|
core := zapcore.NewCore(
|
||||||
|
encoder,
|
||||||
|
writerSyncs,
|
||||||
|
logLevel,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger := zap.New(core, zap.AddCaller(), zap.AddCallerSkip(1), zap.AddStacktrace(zapcore.ErrorLevel))
|
||||||
|
|
||||||
|
instance = &Logger{
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return instance
|
||||||
|
}
|
||||||
|
|
||||||
|
// getUserFields extracts user information from context and returns zap fields
|
||||||
|
func getUserFields(ctx context.Context) []zap.Field {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := []zap.Field{}
|
||||||
|
if userID := web.GetUserID(ctx); userID != "" {
|
||||||
|
fields = append(fields, zap.String("user_id", userID))
|
||||||
|
}
|
||||||
|
if userName := web.GetUserName(ctx); userName != "" {
|
||||||
|
fields = append(fields, zap.String("user_name", userName))
|
||||||
|
}
|
||||||
|
if trace := web.GetTrace(ctx); trace != "" {
|
||||||
|
fields = append(fields, zap.String("trace", trace))
|
||||||
|
}
|
||||||
|
if fromIP := web.GetFromIP(ctx); fromIP != "" {
|
||||||
|
fields = append(fields, zap.String("from_ip", fromIP))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// Info logs an info message
|
||||||
|
func (l *Logger) Info(ctx context.Context, msg string, values ...interface{}) {
|
||||||
|
allFields := getUserFields(ctx)
|
||||||
|
formattedMsg := fmt.Sprintf(msg, values...)
|
||||||
|
if len(values) > 0 {
|
||||||
|
allFields = append(allFields, zap.String("msg", formattedMsg))
|
||||||
|
}
|
||||||
|
l.logger.Info(formattedMsg, allFields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error logs an error message
|
||||||
|
func (l *Logger) Error(ctx context.Context, msg string, values ...interface{}) {
|
||||||
|
allFields := getUserFields(ctx)
|
||||||
|
formattedMsg := fmt.Sprintf(msg, values...)
|
||||||
|
if len(values) > 0 {
|
||||||
|
allFields = append(allFields, zap.String("msg", formattedMsg))
|
||||||
|
}
|
||||||
|
l.logger.Error(formattedMsg, allFields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Debug logs a debug message
|
||||||
|
func (l *Logger) Debug(ctx context.Context, msg string, values ...interface{}) {
|
||||||
|
allFields := getUserFields(ctx)
|
||||||
|
formattedMsg := fmt.Sprintf(msg, values...)
|
||||||
|
if len(values) > 0 {
|
||||||
|
allFields = append(allFields, zap.String("msg", formattedMsg))
|
||||||
|
}
|
||||||
|
l.logger.Debug(formattedMsg, allFields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn logs a warning message
|
||||||
|
func (l *Logger) Warn(ctx context.Context, msg string, values ...interface{}) {
|
||||||
|
allFields := getUserFields(ctx)
|
||||||
|
formattedMsg := fmt.Sprintf(msg, values...)
|
||||||
|
if len(values) > 0 {
|
||||||
|
allFields = append(allFields, zap.String("msg", formattedMsg))
|
||||||
|
}
|
||||||
|
l.logger.Warn(formattedMsg, allFields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fatal logs a fatal message and exits
|
||||||
|
func (l *Logger) Fatal(ctx context.Context, msg string, values ...interface{}) {
|
||||||
|
allFields := getUserFields(ctx)
|
||||||
|
formattedMsg := fmt.Sprintf(msg, values...)
|
||||||
|
if len(values) > 0 {
|
||||||
|
allFields = append(allFields, zap.String("msg", formattedMsg))
|
||||||
|
}
|
||||||
|
l.logger.Fatal(formattedMsg, allFields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync flushes any buffered log entries
|
||||||
|
func (l *Logger) Sync() error {
|
||||||
|
return l.logger.Sync()
|
||||||
|
}
|
||||||
166
logger/logger_test.go
Normal file
166
logger/logger_test.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.hujye.com/infrastructure/go-web-gin/web"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LoggerTestSuite is the test suite for Logger
|
||||||
|
type LoggerTestSuite struct {
|
||||||
|
suite.Suite
|
||||||
|
logger *Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupSuite runs once before all tests
|
||||||
|
func (s *LoggerTestSuite) SetupSuite() {
|
||||||
|
s.logger = GetLogger()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownSuite runs once after all tests
|
||||||
|
func (s *LoggerTestSuite) TearDownSuite() {
|
||||||
|
s.logger.Sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetupTest runs before each test
|
||||||
|
func (s *LoggerTestSuite) SetupTest() {
|
||||||
|
// Reset logger instance if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// TearDownTest runs after each test
|
||||||
|
func (s *LoggerTestSuite) TearDownTest() {
|
||||||
|
// Cleanup after each test if needed
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetLogger tests the GetLogger function
|
||||||
|
func (s *LoggerTestSuite) TestGetLogger() {
|
||||||
|
logger := GetLogger()
|
||||||
|
s.NotNil(logger, "GetLogger should return a non-nil logger")
|
||||||
|
s.NotNil(logger.logger, "logger field should be initialized")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetLoggerSingleton tests that GetLogger returns the same instance
|
||||||
|
func (s *LoggerTestSuite) TestGetLoggerSingleton() {
|
||||||
|
logger1 := GetLogger()
|
||||||
|
logger2 := GetLogger()
|
||||||
|
s.Equal(logger1, logger2, "GetLogger should return the same instance")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInfoWithNilContext tests Info method with nil context
|
||||||
|
func (s *LoggerTestSuite) TestInfoWithNilContext() {
|
||||||
|
s.NotPanics(func() {
|
||||||
|
s.logger.Info(nil, "Test info message")
|
||||||
|
}, "Info should not panic with nil context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestErrorWithNilContext tests Error method with nil context
|
||||||
|
func (s *LoggerTestSuite) TestErrorWithNilContext() {
|
||||||
|
s.NotPanics(func() {
|
||||||
|
s.logger.Error(nil, "Test error message")
|
||||||
|
}, "Error should not panic with nil context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDebugWithNilContext tests Debug method with nil context
|
||||||
|
func (s *LoggerTestSuite) TestDebugWithNilContext() {
|
||||||
|
s.NotPanics(func() {
|
||||||
|
s.logger.Debug(nil, "Test debug message")
|
||||||
|
}, "Debug should not panic with nil context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWarnWithNilContext tests Warn method with nil context
|
||||||
|
func (s *LoggerTestSuite) TestWarnWithNilContext() {
|
||||||
|
s.NotPanics(func() {
|
||||||
|
s.logger.Warn(nil, "Test warn message")
|
||||||
|
}, "Warn should not panic with nil context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInfoWithContext tests Info method with context containing user info
|
||||||
|
func (s *LoggerTestSuite) TestInfoWithContext() {
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx = web.SetUserID(ctx, "test-user-123")
|
||||||
|
ctx = web.SetUserName(ctx, "testuser")
|
||||||
|
ctx = web.SetTrace(ctx, "trace-test-456")
|
||||||
|
ctx = web.SetFromIP(ctx, "192.168.1.1")
|
||||||
|
|
||||||
|
s.NotPanics(func() {
|
||||||
|
s.logger.Info(ctx, "Test info with context")
|
||||||
|
}, "Info should not panic with valid context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestErrorWithContext tests Error method with context containing user info
|
||||||
|
func (s *LoggerTestSuite) TestErrorWithContext() {
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx = web.SetUserID(ctx, "error-user-789")
|
||||||
|
|
||||||
|
s.NotPanics(func() {
|
||||||
|
s.logger.Error(ctx, "Test error with context")
|
||||||
|
}, "Error should not panic with valid context")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestInfoWithFormattedMessage tests Info method with formatted message
|
||||||
|
func (s *LoggerTestSuite) TestInfoWithFormattedMessage() {
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx = web.SetUserID(ctx, "format-user-001")
|
||||||
|
|
||||||
|
s.NotPanics(func() {
|
||||||
|
s.logger.Info(ctx, "User %s logged in from %s", "admin", "10.0.0.1")
|
||||||
|
}, "Info should handle formatted messages correctly")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestErrorWithFormattedMessage tests Error method with formatted message
|
||||||
|
func (s *LoggerTestSuite) TestErrorWithFormattedMessage() {
|
||||||
|
s.NotPanics(func() {
|
||||||
|
s.logger.Error(nil, "Error code: %d, message: %s", 500, "Internal Server Error")
|
||||||
|
}, "Error should handle formatted messages correctly")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAllLogLevels tests all log level methods
|
||||||
|
func (s *LoggerTestSuite) TestAllLogLevels() {
|
||||||
|
ctx := context.Background()
|
||||||
|
ctx = web.SetUserID(ctx, "level-test-user")
|
||||||
|
ctx = web.SetUserName(ctx, "leveltest")
|
||||||
|
ctx = web.SetTrace(ctx, "trace-level-test")
|
||||||
|
ctx = web.SetFromIP(ctx, "10.20.30.40")
|
||||||
|
|
||||||
|
s.NotPanics(func() {
|
||||||
|
s.logger.Debug(ctx, "Debug message")
|
||||||
|
s.logger.Info(ctx, "Info message")
|
||||||
|
s.logger.Warn(ctx, "Warn message")
|
||||||
|
s.logger.Error(ctx, "Error message")
|
||||||
|
// Skip Fatal as it will exit the program
|
||||||
|
}, "All log levels should work correctly")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEmptyContext tests with empty context (no user info)
|
||||||
|
func (s *LoggerTestSuite) TestEmptyContext() {
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
s.NotPanics(func() {
|
||||||
|
s.logger.Info(ctx, "Message with empty context")
|
||||||
|
}, "Should handle empty context without user info")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPartialUserInfo tests with partial user information
|
||||||
|
func (s *LoggerTestSuite) TestPartialUserInfo() {
|
||||||
|
ctx := context.Background()
|
||||||
|
// Only set user ID, not other fields
|
||||||
|
ctx = web.SetUserID(ctx, "partial-user")
|
||||||
|
|
||||||
|
s.NotPanics(func() {
|
||||||
|
s.logger.Info(ctx, "Message with partial user info")
|
||||||
|
}, "Should handle partial user information")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLoggerSync tests the Sync method
|
||||||
|
func (s *LoggerTestSuite) TestLoggerSync() {
|
||||||
|
err := s.logger.Sync()
|
||||||
|
// Sync may return an error for stderr in test environment, which is acceptable
|
||||||
|
s.T().Logf("Sync returned error (may be expected for stderr): %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLoggerRunSuite runs the test suite
|
||||||
|
func TestLoggerRunSuite(t *testing.T) {
|
||||||
|
suite.Run(t, new(LoggerTestSuite))
|
||||||
|
}
|
||||||
74
logger/struct.go
Normal file
74
logger/struct.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LogMsg represents the standard log message structure
|
||||||
|
type LogMsg struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
UserName string `json:"user_name"`
|
||||||
|
TraceID string `json:"trace"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalLogObject implements zapcore.ObjectMarshaler interface
|
||||||
|
// This allows the encoder to properly encode LogMsg struct
|
||||||
|
func (l LogMsg) MarshalLogObject(enc zapcore.ObjectEncoder) error {
|
||||||
|
enc.AddString("user_id", l.UserID)
|
||||||
|
enc.AddString("user_name", l.UserName)
|
||||||
|
enc.AddString("trace", l.TraceID)
|
||||||
|
enc.AddString("ip", l.IP)
|
||||||
|
enc.AddString("msg", l.Msg)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToZapFields converts LogMsg to zap.Field slice for structured logging
|
||||||
|
func (l *LogMsg) ToZapFields() []zap.Field {
|
||||||
|
return []zap.Field{
|
||||||
|
zap.String("user_id", l.UserID),
|
||||||
|
zap.String("user_name", l.UserName),
|
||||||
|
zap.String("trace", l.TraceID),
|
||||||
|
zap.String("ip", l.IP),
|
||||||
|
zap.String("msg", l.Msg),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewLogMsg creates a new LogMsg instance
|
||||||
|
func NewLogMsg(msg string) *LogMsg {
|
||||||
|
return &LogMsg{
|
||||||
|
Msg: msg,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithUserID sets the user ID
|
||||||
|
func (l *LogMsg) WithUserID(userID string) *LogMsg {
|
||||||
|
l.UserID = userID
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithUserName sets the user name
|
||||||
|
func (l *LogMsg) WithUserName(userName string) *LogMsg {
|
||||||
|
l.UserName = userName
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTraceID sets the trace ID
|
||||||
|
func (l *LogMsg) WithTraceID(traceID string) *LogMsg {
|
||||||
|
l.TraceID = traceID
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithIP sets the IP address
|
||||||
|
func (l *LogMsg) WithIP(ip string) *LogMsg {
|
||||||
|
l.IP = ip
|
||||||
|
return l
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMsg sets the message
|
||||||
|
func (l *LogMsg) WithMsg(msg string) *LogMsg {
|
||||||
|
l.Msg = msg
|
||||||
|
return l
|
||||||
|
}
|
||||||
25
server/server.go
Normal file
25
server/server.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import "github.com/gin-gonic/gin"
|
||||||
|
|
||||||
|
// Server represents the HTTP server
|
||||||
|
type Server struct {
|
||||||
|
engine *gin.Engine
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new Server instance
|
||||||
|
func New() *Server {
|
||||||
|
return &Server{
|
||||||
|
engine: gin.Default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Engine returns the gin engine
|
||||||
|
func (s *Server) Engine() *gin.Engine {
|
||||||
|
return s.engine
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts the server
|
||||||
|
func (s *Server) Run(addr ...string) error {
|
||||||
|
return s.engine.Run(addr...)
|
||||||
|
}
|
||||||
213
web/user.go
Normal file
213
web/user.go
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// UserInfo represents user information with tracking details
|
||||||
|
type UserInfo struct {
|
||||||
|
UserID string `json:"user_id"`
|
||||||
|
UserName string `json:"user_name"`
|
||||||
|
Trace string `json:"trace"`
|
||||||
|
VisitTime time.Time `json:"visit_time"`
|
||||||
|
FromIP string `json:"from_ip"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// contextKey is the type for context keys to prevent collisions
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Context keys for each field
|
||||||
|
UserIDKey contextKey = "userID"
|
||||||
|
UserNameKey contextKey = "userName"
|
||||||
|
TraceKey contextKey = "trace"
|
||||||
|
VisitTimeKey contextKey = "visitTime"
|
||||||
|
FromIPKey contextKey = "fromIP"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetUserID returns user ID from context
|
||||||
|
func GetUserID(ctx context.Context) string {
|
||||||
|
if ctx == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if val, ok := ctx.Value(UserIDKey).(string); ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserID sets user ID in context and returns new context
|
||||||
|
func SetUserID(ctx context.Context, userID string) context.Context {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, UserIDKey, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserName returns user name from context
|
||||||
|
func GetUserName(ctx context.Context) string {
|
||||||
|
if ctx == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if val, ok := ctx.Value(UserNameKey).(string); ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetUserName sets user name in context and returns new context
|
||||||
|
func SetUserName(ctx context.Context, userName string) context.Context {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, UserNameKey, userName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTrace returns trace ID from context
|
||||||
|
func GetTrace(ctx context.Context) string {
|
||||||
|
if ctx == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if val, ok := ctx.Value(TraceKey).(string); ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTrace sets trace ID in context and returns new context
|
||||||
|
func SetTrace(ctx context.Context, trace string) context.Context {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, TraceKey, trace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetVisitTime returns visit time from context
|
||||||
|
func GetVisitTime(ctx context.Context) time.Time {
|
||||||
|
if ctx == nil {
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
if val, ok := ctx.Value(VisitTimeKey).(time.Time); ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return time.Time{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetVisitTime sets visit time in context and returns new context
|
||||||
|
func SetVisitTime(ctx context.Context, visitTime time.Time) context.Context {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, VisitTimeKey, visitTime)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetFromIP returns from IP from context
|
||||||
|
func GetFromIP(ctx context.Context) string {
|
||||||
|
if ctx == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if val, ok := ctx.Value(FromIPKey).(string); ok {
|
||||||
|
return val
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetFromIP sets from IP in context and returns new context
|
||||||
|
func SetFromIP(ctx context.Context, fromIP string) context.Context {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
return context.WithValue(ctx, FromIPKey, fromIP)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromContext builds UserInfo from individual context fields
|
||||||
|
// Returns nil if no fields are found
|
||||||
|
func FromContext(ctx context.Context) *UserInfo {
|
||||||
|
if ctx == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
info := &UserInfo{}
|
||||||
|
hasData := false
|
||||||
|
|
||||||
|
if userID := GetUserID(ctx); userID != "" {
|
||||||
|
info.UserID = userID
|
||||||
|
hasData = true
|
||||||
|
}
|
||||||
|
if userName := GetUserName(ctx); userName != "" {
|
||||||
|
info.UserName = userName
|
||||||
|
hasData = true
|
||||||
|
}
|
||||||
|
if trace := GetTrace(ctx); trace != "" {
|
||||||
|
info.Trace = trace
|
||||||
|
hasData = true
|
||||||
|
}
|
||||||
|
if visitTime := GetVisitTime(ctx); !visitTime.IsZero() {
|
||||||
|
info.VisitTime = visitTime
|
||||||
|
hasData = true
|
||||||
|
}
|
||||||
|
if fromIP := GetFromIP(ctx); fromIP != "" {
|
||||||
|
info.FromIP = fromIP
|
||||||
|
hasData = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if !hasData {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToContext sets all UserInfo fields to context and returns new context
|
||||||
|
func ToContext(ctx context.Context, userInfo *UserInfo) context.Context {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
if userInfo == nil {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = SetUserID(ctx, userInfo.UserID)
|
||||||
|
ctx = SetUserName(ctx, userInfo.UserName)
|
||||||
|
ctx = SetTrace(ctx, userInfo.Trace)
|
||||||
|
ctx = SetVisitTime(ctx, userInfo.VisitTime)
|
||||||
|
ctx = SetFromIP(ctx, userInfo.FromIP)
|
||||||
|
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewUserInfo creates a new UserInfo instance
|
||||||
|
func NewUserInfo() *UserInfo {
|
||||||
|
return &UserInfo{
|
||||||
|
VisitTime: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithUserID sets the user ID
|
||||||
|
func (u *UserInfo) WithUserID(userID string) *UserInfo {
|
||||||
|
u.UserID = userID
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithUserName sets the user name
|
||||||
|
func (u *UserInfo) WithUserName(userName string) *UserInfo {
|
||||||
|
u.UserName = userName
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithTrace sets the trace ID
|
||||||
|
func (u *UserInfo) WithTrace(trace string) *UserInfo {
|
||||||
|
u.Trace = trace
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFromIP sets the from IP address
|
||||||
|
func (u *UserInfo) WithFromIP(ip string) *UserInfo {
|
||||||
|
u.FromIP = ip
|
||||||
|
return u
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithVisitTime sets the visit time
|
||||||
|
func (u *UserInfo) WithVisitTime(visitTime time.Time) *UserInfo {
|
||||||
|
u.VisitTime = visitTime
|
||||||
|
return u
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user