Files
2026-07-14 10:24:10 +08:00

293 lines
6.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package console
import (
"fmt"
"io"
"os"
"runtime"
"strings"
"sync"
"sync/atomic"
"time"
)
// writeMu 串行化所有 Console 的写出序列(P1 #7)。
// 用包级锁而非 per-Console 锁:多个 Console 实例可能共享同一底层 writer(如 stdout),
// per-Console 锁无法阻止跨实例交错;且 Windows printColor 的"设色→写→复位"三步必须整体
// 原子,否则并发彩色输出会串色。控制台输出非热路径,单一全局写锁的串行化开销可忽略。
var writeMu sync.Mutex
// Level 日志级别
type Level int32
const (
// LevelDebug 调试级别(最低)。
LevelDebug Level = iota
// LevelInfo 普通信息。
LevelInfo
// LevelSuccess 成功信息。
LevelSuccess
// LevelWarn 警告。
LevelWarn
// LevelError 错误(最高常规级别)。
LevelError
// LevelSilent 完全静默:所有调用都不输出
LevelSilent Level = 127
)
// String 返回级别名称
func (l Level) String() string {
if c, ok := colors[l]; ok {
return c.Name
}
if l == LevelSilent {
return "Silent"
}
return "Unknown"
}
// Color 颜色定义
type Color struct {
Code string
Name string
}
var colors = map[Level]Color{
LevelDebug: {Code: "0;36", Name: "Debug"}, // 青色
LevelInfo: {Code: "0;37", Name: "Info"}, // 白色
LevelSuccess: {Code: "0;92", Name: "Success"}, // 亮绿色
LevelWarn: {Code: "1;93", Name: "Warn"}, // 亮黄色
LevelError: {Code: "1;31", Name: "Error"}, // 亮红色
}
// Console 控制台打印器。
//
// console 包定位:开发期彩色 stdout 工具,跟 fmt.Println 同级。
// 不写文件、不感知运行环境、不做任何隐式 level 切换——
// 所有 level 行为都由调用方显式控制(SetLevel / WithLevel)。
//
// 业务可观测信息(用户登录、订单状态变更等"上线后必须保留的事件")
// 请使用 logger 包;console 仅用于开发期肉眼调试。
type Console struct {
output io.Writer
isColor bool
showTime bool
showCall bool
timeFmt string
skipCall int
// level 通过 atomic 访问,支持运行期热切换且并发安全。
// 用 int32 存储 Level0 = LevelDebug,与零值默认对齐。
level atomic.Int32
}
// Option 配置选项
type Option func(*Console)
// WithOutput 设置输出目标
func WithOutput(w io.Writer) Option {
return func(c *Console) {
c.output = w
}
}
// WithColor 设置是否启用颜色
func WithColor(enable bool) Option {
return func(c *Console) {
c.isColor = enable
}
}
// WithTime 设置是否显示时间
func WithTime(show bool) Option {
return func(c *Console) {
c.showTime = show
}
}
// WithCaller 设置是否显示调用位置。
// skip 可选,默认 2(直接调用方);自封装一层时传 3。
func WithCaller(show bool, skip ...int) Option {
return func(c *Console) {
c.showCall = show
if len(skip) > 0 && skip[0] > 0 {
c.skipCall = skip[0]
}
}
}
// WithTimeFormat 设置时间格式
func WithTimeFormat(fmt string) Option {
return func(c *Console) {
c.timeFmt = fmt
}
}
// WithLevel 设置最低输出级别。低于该级别的调用会被静默丢弃。
//
// 例:WithLevel(LevelWarn) 只输出 Warn 与 Error
//
// WithLevel(LevelSilent) 完全静默,常用于压测或上线观察期临时关闭调试输出。
func WithLevel(l Level) Option {
return func(c *Console) {
c.level.Store(int32(l))
}
}
// New 创建控制台打印器
func New(opts ...Option) *Console {
c := &Console{
output: os.Stdout,
isColor: true,
showTime: true,
showCall: true,
timeFmt: "15:04:05.000",
skipCall: 2,
}
// 默认 LevelDebug:开发期所有级别都打印。生产期请显式 SetLevel/WithLevel。
c.level.Store(int32(LevelDebug))
for _, opt := range opts {
opt(c)
}
return c
}
// SetLevel 运行期切换最低输出级别。并发安全。
func (c *Console) SetLevel(l Level) {
c.level.Store(int32(l))
}
// Level 返回当前最低输出级别
func (c *Console) Level() Level {
return Level(c.level.Load())
}
// Default 默认控制台
var Default = New()
// SetLevel 设置默认控制台的最低输出级别。并发安全。
//
// 典型用法(在 main 中根据 cfg 显式切换):
//
// if cfg.IsProduction() {
// console.SetLevel(console.LevelWarn) // 生产期只看 Warn / Error
// }
//
// 框架不会自动根据环境模式切换,选择权完全在调用方。
func SetLevel(l Level) {
Default.SetLevel(l)
}
// GetLevel 返回默认控制台当前最低输出级别。
// (命名加 Get 前缀是因为 Level 已被类型占用,Go 不允许同名函数。)
func GetLevel() Level {
return Default.Level()
}
// print 内部打印函数
func (c *Console) print(level Level, s ...any) {
// 级别过滤:低于阈值或调用方显式 LevelSilent 时直接返回,零开销
if level < c.Level() {
return
}
var sb strings.Builder
// 时间
if c.showTime {
sb.WriteString(time.Now().Format(c.timeFmt))
sb.WriteString(" ")
}
// 级别名称
color := colors[level]
sb.WriteString("[")
sb.WriteString(color.Name)
sb.WriteString("] ")
// 调用位置
if c.showCall {
sb.WriteString(c.getCaller())
sb.WriteString(" ")
}
// 内容
sb.WriteString(fmt.Sprint(s...))
// 输出。P1 #7:全局写锁串行化整个写序列,避免并发交错输出;
// Windows 下 printColor 的"设色→写→复位"三步也被此锁整体保护,防止串色。
writeMu.Lock()
if c.isColor {
c.printColor(color.Code, sb.String())
} else {
fmt.Fprintln(c.output, sb.String())
}
writeMu.Unlock()
}
// getCaller 获取调用位置
func (c *Console) getCaller() string {
_, file, line, ok := runtime.Caller(c.skipCall)
if !ok {
return ""
}
// 只取文件名(兼容 / 与 \ 分隔)
if idx := strings.LastIndexAny(file, "/\\"); idx >= 0 {
file = file[idx+1:]
}
return fmt.Sprintf("%s:%d", file, line)
}
// Debug 打印调试信息(青色)
func (c *Console) Debug(s ...any) {
c.print(LevelDebug, s...)
}
// Info 打印普通信息(白色)
func (c *Console) Info(s ...any) {
c.print(LevelInfo, s...)
}
// Success 打印成功信息(绿色)
func (c *Console) Success(s ...any) {
c.print(LevelSuccess, s...)
}
// Warn 打印警告信息(黄色)
func (c *Console) Warn(s ...any) {
c.print(LevelWarn, s...)
}
// Error 打印错误信息(红色)
func (c *Console) Error(s ...any) {
c.print(LevelError, s...)
}
// ===== 包级别便捷函数 =====
// Debug 使用默认控制台打印调试信息
func Debug(s ...any) {
Default.print(LevelDebug, s...)
}
// Info 使用默认控制台打印普通信息
func Info(s ...any) {
Default.print(LevelInfo, s...)
}
// Success 使用默认控制台打印成功信息
func Success(s ...any) {
Default.print(LevelSuccess, s...)
}
// Warn 使用默认控制台打印警告信息
func Warn(s ...any) {
Default.print(LevelWarn, s...)
}
// Error 使用默认控制台打印错误信息
func Error(s ...any) {
Default.print(LevelError, s...)
}