refactor(logging): 迁移日志记录到 Zerolog 并优化错误处理
- 将日志记录库从 Logrus 替换为 Zerolog,提升性能和灵活性 - 更新各个模块的日志记录方式,确保一致性 - 优化错误处理逻辑,确保在发生错误时能够正确记录并传递错误信息 - 移除不再使用的错误处理字段,简化代码结构 - 更新相关文档以反映新的日志记录和错误处理机制
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
---
|
||||
name: DBState refactor to lastError
|
||||
overview: 升级 common.Error 基础类型(安全 Error() + HasError() + Unwrap()),删除 common/dbstate.go,新增 db/dberror.go(DBError 嵌入 Error),用 atomic.Pointer 无锁读写。
|
||||
todos:
|
||||
- id: upgrade-error
|
||||
content: 升级 common/error.go:添加安全 Error()、HasError()、Unwrap() 方法,更新注释
|
||||
status: completed
|
||||
- id: delete-dbstate
|
||||
content: 删除 common/dbstate.go
|
||||
status: completed
|
||||
- id: add-dberror-type
|
||||
content: 新增 db/dberror.go(DBError 嵌入 Error),db/db.go 添加 atomic.Pointer[DBError] + GetLastError()
|
||||
status: completed
|
||||
- id: update-db-internals
|
||||
content: 更新 db/query.go、transaction.go、crud.go、db.go 中所有 lastState 调用
|
||||
status: completed
|
||||
- id: update-examples
|
||||
content: 更新 example/app/test.go 和 mysql.go
|
||||
status: completed
|
||||
- id: build-and-test
|
||||
content: 编译 + 运行测试验证
|
||||
status: completed
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Error 升级 + DBState 重构
|
||||
|
||||
## 一、升级 common.Error 基础类型
|
||||
|
||||
当前 [common/error.go](common/error.go) 只有 `GetError()`/`SetError()`,存在问题:
|
||||
|
||||
- `Error()` 方法由嵌入的 `error` 接口提供,**error 为 nil 时调用会 panic**
|
||||
- 缺少 `errors.Is`/`errors.As` 支持(Go 标准错误链)
|
||||
- 判断有无错误要写 `e.GetError() != nil`,不够简洁
|
||||
|
||||
升级后(**向后兼容,纯增量**):
|
||||
|
||||
```go
|
||||
type Error struct {
|
||||
error
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// Error 安全实现 error 接口 —— nil 时返回 "" 而非 panic
|
||||
func (that *Error) Error() string {
|
||||
that.mu.RLock()
|
||||
defer that.mu.RUnlock()
|
||||
if that.error == nil {
|
||||
return ""
|
||||
}
|
||||
return that.error.Error()
|
||||
}
|
||||
|
||||
// HasError 快捷判断(替代 GetError() != nil)
|
||||
func (that *Error) HasError() bool {
|
||||
that.mu.RLock()
|
||||
defer that.mu.RUnlock()
|
||||
return that.error != nil
|
||||
}
|
||||
|
||||
// Unwrap 支持 errors.Is / errors.As 标准错误链
|
||||
func (that *Error) Unwrap() error {
|
||||
that.mu.RLock()
|
||||
defer that.mu.RUnlock()
|
||||
return that.error
|
||||
}
|
||||
|
||||
// GetError / SetError 保持不变
|
||||
```
|
||||
|
||||
## 二、删除 common/dbstate.go
|
||||
|
||||
删除 `DBState` 和 `DBSnapshot` 类型,DB 专用类型不该放 common。
|
||||
|
||||
## 三、新增 db/dberror.go —— 嵌入 Error,框架统一
|
||||
|
||||
```go
|
||||
package db
|
||||
|
||||
import . "code.hoteas.com/golang/hotime/common"
|
||||
|
||||
// DBError 最后一次 SQL 操作的结构化快照
|
||||
// 嵌入框架 Error 类型,与 Obj 等保持一致的错误 API
|
||||
type DBError struct {
|
||||
Error // 嵌入 common.Error:HasError()/GetError()/Unwrap() 直接可用
|
||||
Query string
|
||||
Data []interface{}
|
||||
}
|
||||
```
|
||||
|
||||
应用层使用体验与 Obj 一致:
|
||||
|
||||
```go
|
||||
// 判断出错(与 Obj 同样的 API)
|
||||
if db.GetLastError().HasError() { /* SQL 出错 */ }
|
||||
|
||||
// 取原始 error
|
||||
rawErr := db.GetLastError().GetError()
|
||||
|
||||
// Go 标准错误链判断
|
||||
if errors.Is(db.GetLastError(), sql.ErrNoRows) { ... }
|
||||
|
||||
// 结构化字段(调试/SQL 监测系统)
|
||||
e := db.GetLastError()
|
||||
fmt.Println(e.Query, e.Data)
|
||||
```
|
||||
|
||||
## 四、HoTimeDB:atomic.Pointer 无锁读写
|
||||
|
||||
```go
|
||||
type HoTimeDB struct {
|
||||
// ...
|
||||
lastError atomic.Pointer[DBError]
|
||||
}
|
||||
|
||||
func (db *HoTimeDB) GetLastError() *DBError {
|
||||
return db.lastError.Load()
|
||||
}
|
||||
```
|
||||
|
||||
## 五、内部写入:替换所有 lastState 调用
|
||||
|
||||
每次 SQL 执行后创建新 DBError 并原子 Store:
|
||||
|
||||
```go
|
||||
e := &DBError{Query: query, Data: args}
|
||||
e.SetError(sqlErr) // 用框架 Error 的 SetError
|
||||
that.lastError.Store(e)
|
||||
```
|
||||
|
||||
具体替换点:
|
||||
|
||||
- [db/query.go](db/query.go):2 处 `lastState.Set(query, args, sqlErr)` + 2 处 `lastState.SetError(err)`
|
||||
- [db/db.go](db/db.go):InitDb 中 2 处 `lastState.SetError(e)`
|
||||
- [db/transaction.go](db/transaction.go):3 处 `lastState.SetError(err)`
|
||||
- [db/crud.go](db/crud.go):1 处 `lastState.SetError(e)`
|
||||
|
||||
## 六、外部调用适配
|
||||
|
||||
- [example/app/test.go](example/app/test.go):22 处 `GetLastState().Query` -> `GetLastError().Query`
|
||||
- [example/app/mysql.go](example/app/mysql.go):3 处,同上
|
||||
|
||||
始终记录(成功时 `.GetError() == nil`),`.Query` 可直接安全读取。
|
||||
|
||||
## 七、与 Logger 的分工
|
||||
|
||||
- Logger(`logSQL`):格式化输出(带日期、caller、级别)—— 人看的
|
||||
- `lastError`:结构化原始数据(Query/Data/原始 error)—— 程序读的
|
||||
- 零重复:原始 error 只存一份
|
||||
|
||||
## 改动文件清单
|
||||
|
||||
- `common/error.go` — 添加 `Error()`/`HasError()`/`Unwrap()` 方法,更新注释
|
||||
- `common/dbstate.go` — 删除
|
||||
- `db/dberror.go` — 新增:DBError struct(嵌入 Error)
|
||||
- `db/db.go` — `atomic.Pointer[DBError]` 字段 + `GetLastError()` 方法
|
||||
- `db/query.go` — `lastState.Set` -> `lastError.Store`
|
||||
- `db/transaction.go` — `lastState.SetError` -> `lastError.Store`
|
||||
- `db/crud.go` — 同上
|
||||
- `example/app/test.go` — `GetLastState()` -> `GetLastError()`
|
||||
- `example/app/mysql.go` — 同上
|
||||
@@ -0,0 +1,424 @@
|
||||
---
|
||||
name: Logging System Redesign
|
||||
overview: Replace logrus with zerolog, remove mode config, redesign logLevel/logFile, simplify Error to pure storage, add DBState for atomic DB state access, and provide a clean application-layer logging API.
|
||||
todos:
|
||||
- id: add-zerolog-dep
|
||||
content: 添加 zerolog 依赖到 go.mod,移除 logrus(vendor 中 wechat 等三方库自带的 logrus 不影响)
|
||||
status: completed
|
||||
- id: rewrite-log-package
|
||||
content: 重写 log/ 包:基于 zerolog 实现 Logger 结构体、NewLogger 工厂、CallerMarshalFunc 智能调用者过滤、TemplateFileWriter 带缓冲和模板路径切换
|
||||
status: completed
|
||||
- id: delete-error-add-dbstate
|
||||
content: 删除 common/error.go(Error 类型不再需要)、新增 common/dbstate.go(DBState 包装 Query+Data+Error)、Logger 内置 ERROR 级别环形缓冲
|
||||
status: completed
|
||||
- id: update-application
|
||||
content: 改造 application.go:删除 mode 配置、Application.Log 类型变更、SetConfig 中日志初始化用 logLevel 控制一切、DB 缓存始终开启、Cache-Control 跟 logLevel 联动
|
||||
status: completed
|
||||
- id: update-db
|
||||
content: 改造 db/db.go 和 db/query.go:用 DBState 替换 LastQuery+LastData+LastErr、HoTimeDB.Log 类型变更、SQL 日志格式优化、消除重复打印
|
||||
status: completed
|
||||
- id: update-var-config
|
||||
content: 更新 var.go 默认配置(删 mode、logLevel 默认 3)和 ConfigNote、example 配置文件
|
||||
status: completed
|
||||
- id: update-codegen
|
||||
content: 适配 code/makecode.go 中的 Error 使用(移除 Logger 相关)
|
||||
status: completed
|
||||
- id: update-tests
|
||||
content: 适配 testing_helper.go 和 db/transaction_test.go 中的日志创建
|
||||
status: completed
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# HoTime 日志系统全面重写方案
|
||||
|
||||
## 一、现状问题分析
|
||||
|
||||
### 1. logrus 已不适合继续使用
|
||||
- logrus 已进入 **维护模式**,不再开发新功能,仅修复安全问题
|
||||
- 性能差:依赖 mutex 锁和反射,在高并发下显著慢于现代替代品
|
||||
- 当前使用版本 `v1.8.1`([go.mod](go.mod) line 13)
|
||||
|
||||
### 2. 现有日志配置形同虚设
|
||||
- `logLevel` 在 [var.go](var.go) line 59 有定义和注释,但 **从未在代码中接入 logrus**
|
||||
- `mode` 承担了过多职责(0/1/2/3 四种),但代码中实际只区分 `== 0` 和 `!= 0`,且 mode 2/3 无独立分支
|
||||
|
||||
### 3. Error 类型与日志耦合导致重复打印
|
||||
- `SetError` 内部隐式 `Warn` 打印 + `query.go` defer 再打一次 = **同一错误打印两次**
|
||||
- `LastQuery`/`LastData`/`LastErr` 用不同的锁保护,存在读到不一致状态的风险
|
||||
|
||||
---
|
||||
|
||||
## 二、日志库选型:zerolog
|
||||
|
||||
**推荐 [zerolog](https://github.com/rs/zerolog)**:
|
||||
- 零内存分配,性能远超 logrus(快 5-10 倍)
|
||||
- 内置 `Caller()`、`ConsoleWriter`(彩色)、`MultiLevelWriter`(多输出)
|
||||
- 无 Go 版本要求(go.mod 保持 `go 1.16`)
|
||||
- 不选 `log/slog`:需 Go 1.21+,升级成本太大
|
||||
|
||||
---
|
||||
|
||||
## 三、配置参数重新设计
|
||||
|
||||
### 删除 `mode`
|
||||
|
||||
`mode` 原来控制的行为全部转移:
|
||||
- **DB 缓存**:`mode` 的拦截是多余的,cache 本身已是"配置即启用"设计(`cache.memory.db`/`cache.redis.db` 等控制)。删除 `mode` 后 `SetCache` 中始终挂载 `that.Db.HoTimeCache`,是否真正缓存由 cache 配置决定
|
||||
- **Cache-Control**:跟 `logLevel` 联动 -- `logLevel > 0` 时设 `no-cache`,`logLevel == 0` 时 `public`
|
||||
- **SQL 日志打印**:完全由 `logLevel` 控制(`logLevel > 0` 时打印 SQL)
|
||||
- **代码生成**:本来就由 `codeConfig[].mode` 独立控制,不受影响
|
||||
|
||||
### `logLevel` -- 保留,真正接管日志系统
|
||||
|
||||
- `0` = 仅 error(生产环境推荐)
|
||||
- `>=1` = 全部打印(info/warn/error/debug/SQL 全开)
|
||||
- **默认值 `1`**
|
||||
|
||||
简单二分:要么只看错误,要么全看。Cache-Control 同理:`logLevel==0` 时 `public`,`>0` 时 `no-cache`。
|
||||
|
||||
### `logFile` -- 保留
|
||||
|
||||
路径模板不变(`a/b/c/20060102150405.txt`),应用日志写入此文件。
|
||||
|
||||
### `logHistory` -- 新增
|
||||
|
||||
Logger 错误历史缓冲条数,默认 `100`。设为 `0` 关闭历史记录。可根据设备内存情况调小。
|
||||
|
||||
### `webConnectLogShow` / `webConnectLogFile` -- 保留独立
|
||||
|
||||
保留访问日志独立的理由(业务角度):
|
||||
- 访问日志量远大于应用日志,混在一起文件膨胀、查找困难
|
||||
- 生产环境需要不同的轮转策略
|
||||
- Nginx/Apache 都是 access.log 和 error.log 分开的行业惯例
|
||||
|
||||
配置保持不变:
|
||||
- `webConnectLogShow`:默认 true
|
||||
- `webConnectLogFile`:独立文件路径
|
||||
|
||||
---
|
||||
|
||||
## 四、新日志包设计 (`log/`)
|
||||
|
||||
### 核心结构
|
||||
|
||||
```go
|
||||
package log
|
||||
|
||||
type Logger struct {
|
||||
zl zerolog.Logger
|
||||
}
|
||||
|
||||
func NewLogger(logLevel int, logFile string, showCaller bool) *Logger
|
||||
|
||||
func (l *Logger) Debug() *zerolog.Event
|
||||
func (l *Logger) Info() *zerolog.Event
|
||||
func (l *Logger) Warn() *zerolog.Event
|
||||
func (l *Logger) Error() *zerolog.Event
|
||||
|
||||
func (l *Logger) Debugf(format string, v ...interface{})
|
||||
func (l *Logger) Infof(format string, v ...interface{})
|
||||
func (l *Logger) Warnf(format string, v ...interface{})
|
||||
func (l *Logger) Errorf(format string, v ...interface{})
|
||||
```
|
||||
|
||||
### 控制台输出格式
|
||||
|
||||
```
|
||||
2026-04-12 15:04:05 |INFO| 用户登录成功 [app/admin/user.go:45]
|
||||
2026-04-12 15:04:05 |WARN| 数据库连接超时 [app/admin/user.go:78]
|
||||
2026-04-12 15:04:05 |ERROR| 支付回调失败 order_id=456 [app/payment/callback.go:32]
|
||||
```
|
||||
|
||||
格式:`时间 | 类型标志 | 日志内容 | [代码位置]`,用 `[file:line]` 而非 logrus 的 `line="file:line"`。
|
||||
|
||||
### 文件输出格式(JSON)
|
||||
|
||||
```json
|
||||
{"time":"2026-04-12T15:04:05+08:00","level":"info","caller":"app/admin/user.go:45","message":"用户登录成功"}
|
||||
```
|
||||
|
||||
### 调用者智能过滤
|
||||
|
||||
复用并优化现有 [log/logrus.go](log/logrus.go) 中 `findCaller` / `isHoTimeFrameworkFile` 逻辑,实现为 zerolog 的 `CallerMarshalFunc`。
|
||||
|
||||
### 文件写入优化
|
||||
|
||||
`TemplateFileWriter`:带缓冲的 Writer,按时间模板自动切换文件路径,保持 `time.Now().Format(path)` 语义。
|
||||
|
||||
---
|
||||
|
||||
## 五、删除 Error 类型 + DBState + Logger 错误历史
|
||||
|
||||
### 删除 common.Error
|
||||
|
||||
`common.Error` 完全删除,不再需要。原因:
|
||||
|
||||
- Error 存在的意义是"存错误 + 自动打印",打印已移到 Log,只剩存储
|
||||
- 审查全部用法后,每个 `SetError` 调用点都可以直接用 `Log.Error()` 替代
|
||||
- `InitDb` 返回 `*Error` 从未被使用(调用方都是 `_ = that.InitDb(...)`)
|
||||
- `ConnectFunc(err ...*Error)` 可简化为返回 error
|
||||
- 需要"查最近的错误"由 Logger 错误历史替代
|
||||
|
||||
### Logger 错误历史(环形缓冲)
|
||||
|
||||
Logger 内置 ERROR 级别记录的环形缓冲(默认 100 条),打印即存储,零额外操作:
|
||||
|
||||
```go
|
||||
type ErrorRecord struct {
|
||||
Err error
|
||||
Msg string
|
||||
Time time.Time
|
||||
Caller string
|
||||
}
|
||||
|
||||
// Logger 内部
|
||||
type Logger struct {
|
||||
zl zerolog.Logger
|
||||
errors []ErrorRecord // 环形缓冲
|
||||
errorsMu sync.RWMutex
|
||||
maxErrors int // 默认 100
|
||||
}
|
||||
|
||||
// 每次 Log.Error() 自动存入缓冲
|
||||
// 获取最近 N 条错误(不传则返回全部已存储的,不超过 maxErrors)
|
||||
func (l *Logger) GetRecentErrors(n ...int) []ErrorRecord
|
||||
```
|
||||
|
||||
缓冲大小通过配置项 `logHistory` 控制,默认 100,可根据设备内存调整。
|
||||
|
||||
应用层使用:
|
||||
```go
|
||||
// 正常打印,自动存入历史
|
||||
that.Log.Error().Err(err).Msg("支付失败")
|
||||
|
||||
// 获取最近 10 条错误
|
||||
for _, e := range that.Log.GetRecentErrors(10) {
|
||||
fmt.Printf("[%s] %s: %v %s\n", e.Time.Format("15:04:05"), e.Msg, e.Err, e.Caller)
|
||||
}
|
||||
|
||||
// 获取全部已存储的错误
|
||||
all := that.Log.GetRecentErrors()
|
||||
```
|
||||
|
||||
### DBState -- DB 操作状态原子包装(新增)
|
||||
|
||||
新增 [common/dbstate.go](common/dbstate.go),将 `LastQuery` + `LastData` + `LastErr` 合为一体,一把锁保护原子读写。
|
||||
|
||||
**核心语义**:记录最后一次 DB 操作的完整状态。每次 SQL 执行后更新,用于判断"SQL 执行失败"还是"执行成功但结果为空"(如 UPDATE 影响 0 行)。
|
||||
|
||||
```go
|
||||
// DBSnapshot 操作状态快照(只读,安全传递)
|
||||
type DBSnapshot struct {
|
||||
Query string
|
||||
Data []interface{}
|
||||
Err error
|
||||
}
|
||||
|
||||
type DBState struct {
|
||||
query string
|
||||
data []interface{}
|
||||
err error
|
||||
mu sync.RWMutex // 零值即可用
|
||||
}
|
||||
|
||||
// Set 原子更新完整状态(每次 Query/Exec 执行后调用)
|
||||
func (s *DBState) Set(query string, data []interface{}, err error) {
|
||||
s.mu.Lock()
|
||||
s.query = query
|
||||
s.data = data
|
||||
s.err = err
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// Get 返回一致性快照(query/data/err 保证是同一次操作的)
|
||||
func (s *DBState) Get() DBSnapshot {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return DBSnapshot{Query: s.query, Data: s.data, Err: s.err}
|
||||
}
|
||||
|
||||
// SetError 只更新错误(Ping 等无 SQL 的场景)
|
||||
func (s *DBState) SetError(err error) {
|
||||
s.mu.Lock()
|
||||
s.err = err
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
// GetError 快速判断最后操作是否出错
|
||||
func (s *DBState) GetError() error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.err
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
**使用场景**:
|
||||
```go
|
||||
db.Exec("UPDATE stats SET count=count+1 WHERE id=?", id)
|
||||
rows, _ := result.RowsAffected()
|
||||
if rows == 0 {
|
||||
if state := db.GetLastState(); state != nil {
|
||||
// SQL 执行失败,state.Err 有具体错误
|
||||
} else {
|
||||
// SQL 成功,只是没有匹配的行
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### HoTimeDB 结构体变更
|
||||
|
||||
`HoTimeDB` 中原来的三个独立字段合并为私有 `lastState`,通过方法暴露,同时删除 `Mode`:
|
||||
|
||||
```go
|
||||
type HoTimeDB struct {
|
||||
*sql.DB
|
||||
// ...
|
||||
Log *log.Logger // 从 *logrus.Logger 改为新 Logger
|
||||
lastState DBState // 私有,替换 LastQuery + LastData + LastErr
|
||||
// 删除: Mode int
|
||||
// 删除: LastQuery string
|
||||
// 删除: LastData []interface{}
|
||||
// 删除: LastErr *Error
|
||||
}
|
||||
|
||||
// 唯一对外方法:nil = 成功,非 nil = 失败(附带完整上下文)
|
||||
func (db *HoTimeDB) GetLastState() *DBSnapshot
|
||||
```
|
||||
|
||||
内部逻辑:`lastState` 始终记录 query/data/err(供日志使用),但 `GetLastState()` 只在有错误时返回非 nil:
|
||||
```go
|
||||
func (db *HoTimeDB) GetLastState() *DBSnapshot {
|
||||
db.lastState.mu.RLock()
|
||||
defer db.lastState.mu.RUnlock()
|
||||
if db.lastState.err == nil {
|
||||
return nil // 没错误,不需要关心细节
|
||||
}
|
||||
return &DBSnapshot{
|
||||
Query: db.lastState.query,
|
||||
Data: db.lastState.data,
|
||||
Err: db.lastState.err,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
应用层使用:
|
||||
```go
|
||||
// 一行判断 + 取详情
|
||||
if state := db.GetLastState(); state != nil {
|
||||
// SQL 出错了,state.Err / state.Query / state.Data 全有
|
||||
that.Log.Error().Err(state.Err).Str("sql", state.Query).Msg("SQL 执行失败")
|
||||
}
|
||||
// nil = SQL 执行成功,不用管
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、DB 日志格式优化
|
||||
|
||||
### 修复重复打印
|
||||
|
||||
`SetError` 不再打印日志,`query.go` defer 统一负责 SQL 日志 -- 问题根除。
|
||||
|
||||
### 输出格式
|
||||
|
||||
**无错误时(DEBUG 级别):**
|
||||
```
|
||||
2026-04-12 15:04:05 |DEBUG| SQL: SELECT id,worker_id FROM `dingtalk` WHERE `state`=? DATA: [0] [dd/sync.go:63]
|
||||
```
|
||||
|
||||
**有错误时(自动升为 ERROR 级别):**
|
||||
```
|
||||
2026-04-12 15:04:05 |ERROR| SQL: SELECT id,worker_id FROM `dingtalk` WHERE `state`=? DATA: [0] ERROR: connection refused [dd/sync.go:63]
|
||||
```
|
||||
|
||||
关键改动:
|
||||
- 无错误时不显示 `ERROR:<nil>`
|
||||
- 有错误时日志级别自动升为 ERROR(而非一律 INFO)
|
||||
- SQL 日志在 `logLevel > 0` 时输出,`logLevel == 0` 时只有 SQL 出错才打印
|
||||
|
||||
### 测试模式下 SQL 错误自动 fail
|
||||
|
||||
测试框架中 SQL 出错让测试用例直接失败报错。
|
||||
|
||||
---
|
||||
|
||||
## 七、需要修改的文件清单
|
||||
|
||||
### 核心改动
|
||||
|
||||
- [log/logrus.go](log/logrus.go) -- 全部重写为 zerolog 实现,重命名为 `log/logger.go`
|
||||
- [common/error.go](common/error.go) -- **删除整个文件**
|
||||
- 新增 [common/dbstate.go](common/dbstate.go) -- DBState + DBSnapshot 类型
|
||||
- [application.go](application.go) -- 删除 `Error` 嵌入和所有 `Error{}` 初始化、删除 mode 逻辑、`Application.Log` 类型变更、`SetCache` 去掉 mode 判断、Cache-Control 改跟 logLevel、删除 `Db.Mode` 赋值(3 处)、`ConnectFunc` 签名简化、logLevel 接入
|
||||
- [var.go](var.go) -- 删除 `"mode": 2` 默认值、ConfigNote 删除 mode 说明、logLevel 默认值改为 3 并更新说明
|
||||
- [db/db.go](db/db.go) -- 删除 `Mode int` / `LastQuery` / `LastData` / `LastErr` 字段,改为 `LastState DBState` + `Log *log.Logger`,移除 logrus import
|
||||
- [db/query.go](db/query.go) -- 用 `LastState.Set()` 原子写入替换分散的赋值、SQL 日志格式优化(无错不显示 ERROR、有错升 ERROR 级别)、`Mode != 0` 改为 Logger 级别过滤
|
||||
- [go.mod](go.mod) -- 添加 zerolog 依赖
|
||||
|
||||
### 适配改动
|
||||
|
||||
- [db/crud.go](db/crud.go) -- `that.LastErr.SetError(e)` 改为 `that.lastState.SetError(e)`
|
||||
- [db/transaction.go](db/transaction.go) -- 事务拷贝中 `LastQuery`/`LastData`/`LastErr` 改为 `lastState`
|
||||
- [db/transaction_test.go](db/transaction_test.go) -- 删除 `Error{Logger: logger}`、logrus 替换
|
||||
- [code/makecode.go](code/makecode.go) -- 删除嵌入的 `Error`,改用 `*Logger` 打印错误
|
||||
- [cache/cache.go](cache/cache.go) -- `Init` 参数删除 `*Error`,改传 `*Logger`
|
||||
- [cache/cache_redis.go](cache/cache_redis.go) -- `that.Error.SetError(err)` 全部改为 `that.Log.Error().Err(err).Msg(...)`
|
||||
- [testing_helper.go](testing_helper.go) -- `app.Db.Mode` 删除、Logger 创建适配
|
||||
- [context.go](context.go) -- `that.Error.SetError(...)` 改为 `that.Log.Error().Msg(...)`
|
||||
- [example/app/test.go](example/app/test.go) -- `that.Db.LastQuery` 改为 `that.Db.GetLastState()` 相关访问
|
||||
- [example/app/mysql.go](example/app/mysql.go) -- 同上
|
||||
- [example/batch_cache_tester.go](example/batch_cache_tester.go) -- `&Error{}` 删除
|
||||
- [example/config/configNote.json](example/config/configNote.json) -- 删除 mode 说明、更新 logLevel 说明
|
||||
- [example/config/config.json](example/config/config.json) -- 删除 mode
|
||||
|
||||
---
|
||||
|
||||
## 八、应用层使用示例
|
||||
|
||||
```go
|
||||
// 基本使用
|
||||
that.Log.Infof("用户 %s 登录成功", name)
|
||||
that.Log.Errorf("订单 %d 支付失败: %v", orderId, err)
|
||||
|
||||
// 链式调用(附加结构化字段)
|
||||
that.Log.Info().Str("user", name).Int("age", 25).Msg("用户登录成功")
|
||||
that.Log.Error().Err(err).Str("order", orderId).Msg("支付回调失败")
|
||||
|
||||
// 打印即存储(Log.Error 自动存入错误历史)
|
||||
that.Log.Error().Err(err).Msg("数据库连接失败")
|
||||
|
||||
// DB 状态:nil = 成功,非 nil = 失败
|
||||
if state := that.Db.GetLastState(); state != nil {
|
||||
that.Log.Error().Err(state.Err).Str("sql", state.Query).Msg("SQL 失败")
|
||||
}
|
||||
|
||||
// 调阅最近 5 条错误(调试用,包括 DB 错误在内的所有 ERROR 级别日志)
|
||||
errors := that.Log.GetRecentErrors(5)
|
||||
|
||||
// SQL 日志由框架自动处理,logLevel>0 时输出
|
||||
// 无错误: |INFO| SQL: SELECT... DATA: [1,2] [app/user.go:45]
|
||||
// 有错误: |ERROR| SQL: SELECT... DATA: [1,2] ERROR: xxx [app/user.go:45]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、向后兼容与影响范围
|
||||
|
||||
### 编译级 breaking changes
|
||||
|
||||
- `common.Error` 类型完全删除:所有嵌入 `Error` 或传递 `*Error` 的地方改为 `*Logger` 或 `error`
|
||||
- `Application.Log` 从 `*logrus.Logger` 改为 `*log.Logger`
|
||||
- `Application.Error` 字段删除
|
||||
- `ConnectFunc` 签名简化(不再传递 `*Error`)
|
||||
- `HoTimeDB.LastQuery`/`LastData`/`LastErr`/`Mode` 删除:
|
||||
- 应用层通过 `db.GetLastState()` 获取(nil=成功)
|
||||
- 框架内部通过 `db.lastState.SetError(e)` 更新
|
||||
- 删除 `mode` 配置
|
||||
|
||||
### 不受影响
|
||||
|
||||
- `codeConfig[].mode`(代码生成模式)完全不受影响
|
||||
- `modeRouterStrict`(路由严格模式)完全不受影响
|
||||
- `cache` 配置结构不受影响(`cache.db.mode` 等是缓存自己的 mode,与全局 mode 无关)
|
||||
- `dri/mongodb` 的 `LastErr error` 是独立的普通 error,不受影响
|
||||
+61
-80
@@ -2,7 +2,6 @@ package hotime
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -16,21 +15,19 @@ import (
|
||||
"code.hoteas.com/golang/hotime/code"
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
. "code.hoteas.com/golang/hotime/db"
|
||||
. "code.hoteas.com/golang/hotime/log"
|
||||
"github.com/sirupsen/logrus"
|
||||
"code.hoteas.com/golang/hotime/log"
|
||||
)
|
||||
|
||||
type Application struct {
|
||||
MakeCodeRouter map[string]*code.MakeCode
|
||||
MethodRouter
|
||||
Router
|
||||
Error
|
||||
Log *logrus.Logger
|
||||
WebConnectLog *logrus.Logger
|
||||
Log *log.Logger
|
||||
WebConnectLog *log.Logger
|
||||
Port string //端口号
|
||||
TLSPort string //ssl访问端口号
|
||||
connectListener []func(that *Context) bool //所有的访问监听,true按原计划继续使用,false表示有监听器处理
|
||||
connectDbFunc func(err ...*Error) (master, slave *sql.DB)
|
||||
connectDbFunc func() (master, slave *sql.DB)
|
||||
configPath string
|
||||
Config Map
|
||||
Db HoTimeDB
|
||||
@@ -129,7 +126,7 @@ func (that *Application) Run(router Router) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
//that.SetError(errors.New(fmt.Sprint(err)), LOG_FMT)
|
||||
that.Log.Warn(err)
|
||||
that.Log.Warnf("%v", err)
|
||||
|
||||
that.Run(router)
|
||||
}
|
||||
@@ -149,7 +146,7 @@ func (that *Application) Run(router Router) {
|
||||
//启动服务
|
||||
that.Server.Addr = ":" + that.Port
|
||||
err := that.Server.ListenAndServe()
|
||||
that.Log.Error(err)
|
||||
that.Log.Error().Err(err).Msg("HTTP 服务退出")
|
||||
ch <- 1
|
||||
|
||||
}()
|
||||
@@ -163,33 +160,33 @@ func (that *Application) Run(router Router) {
|
||||
//启动服务
|
||||
that.Server.Addr = ":" + that.TLSPort
|
||||
err := that.Server.ListenAndServeTLS(that.Config.GetString("tlsCert"), that.Config.GetString("tlsKey"))
|
||||
that.Log.Error(err)
|
||||
that.Log.Error().Err(err).Msg("HTTPS 服务退出")
|
||||
ch <- 2
|
||||
|
||||
}()
|
||||
}
|
||||
if ObjToCeilInt(that.Port) == 0 && ObjToCeilInt(that.TLSPort) == 0 {
|
||||
that.Log.Error("没有端口启用")
|
||||
that.Log.Errorf("没有端口启用")
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
value := <-ch
|
||||
|
||||
that.Log.Error("启动服务失败 : " + ObjToStr(value))
|
||||
that.Log.Errorf("启动服务失败 : %s", ObjToStr(value))
|
||||
}
|
||||
|
||||
// SetConnectDB 启动实例
|
||||
func (that *Application) SetConnectDB(connect func(err ...*Error) (master, slave *sql.DB)) {
|
||||
func (that *Application) SetConnectDB(connect func() (master, slave *sql.DB)) {
|
||||
|
||||
that.connectDbFunc = connect
|
||||
|
||||
that.Db.SetConnect(that.connectDbFunc, &that.Error)
|
||||
that.Db.SetConnect(that.connectDbFunc)
|
||||
|
||||
}
|
||||
|
||||
// SetDefault 默认配置缓存和session实现
|
||||
func (that *Application) SetDefault(connect func(err ...*Error) (*sql.DB, *sql.DB)) {
|
||||
func (that *Application) SetDefault(connect func() (*sql.DB, *sql.DB)) {
|
||||
that.SetConfig()
|
||||
|
||||
if connect != nil {
|
||||
@@ -202,19 +199,15 @@ func (that *Application) SetDefault(connect func(err ...*Error) (*sql.DB, *sql.D
|
||||
// SetCache 设置配置文件路径全路径或者相对路径
|
||||
func (that *Application) SetCache() {
|
||||
cacheIns := HoTimeCache{}
|
||||
cacheIns.Init(that.Config.GetMap("cache"), HoTimeDBInterface(&that.Db), &that.Error)
|
||||
cacheIns.Init(that.Config.GetMap("cache"), HoTimeDBInterface(&that.Db), that.Log)
|
||||
that.HoTimeCache = &cacheIns
|
||||
//mode生产模式开启的时候才开启数据库缓存,防止调试出问题
|
||||
if that.Config.GetInt("mode") == 0 {
|
||||
that.Db.HoTimeCache = &cacheIns
|
||||
}
|
||||
that.Db.HoTimeCache = &cacheIns
|
||||
}
|
||||
|
||||
// SetConfig 设置配置文件路径全路径或者相对路径
|
||||
func (that *Application) SetConfig(configPath ...string) {
|
||||
|
||||
that.Log = GetLog("", true)
|
||||
that.Error = Error{Logger: that.Log}
|
||||
that.Log = log.NewLogger(1, "", 100)
|
||||
if len(configPath) != 0 {
|
||||
that.configPath = configPath[0]
|
||||
}
|
||||
@@ -225,53 +218,45 @@ func (that *Application) SetConfig(configPath ...string) {
|
||||
//加载配置文件
|
||||
btes, err := ioutil.ReadFile(that.configPath)
|
||||
that.Config = DeepCopyMap(Config).(Map)
|
||||
configErr := false
|
||||
if err == nil {
|
||||
|
||||
cmap := Map{}
|
||||
//文件是否损坏
|
||||
cmap.JsonToMap(string(btes), &that.Error)
|
||||
cmap.JsonToMap(string(btes))
|
||||
|
||||
for k, v := range cmap {
|
||||
that.Config[k] = v //程序配置
|
||||
Config[k] = v //系统配置
|
||||
that.Config[k] = v
|
||||
Config[k] = v
|
||||
}
|
||||
} else {
|
||||
that.Log.Error("配置文件不存在,或者配置出错,使用缺省默认配置")
|
||||
|
||||
that.Log.Errorf("配置文件不存在,或者配置出错,使用缺省默认配置")
|
||||
}
|
||||
|
||||
if that.Error.GetError() != nil {
|
||||
fmt.Println(that.Error.GetError().Error())
|
||||
}
|
||||
|
||||
//文件如果损坏则不写入配置防止配置文件数据丢失
|
||||
if that.Error.GetError() == nil {
|
||||
//var configByte bytes.Buffer
|
||||
|
||||
//判断配置文件是否序列有变化,有则修改配置,无则不变
|
||||
//fmt.Println(len(btes))
|
||||
if !configErr {
|
||||
configStr := that.Config.ToJsonString()
|
||||
if len(btes) != 0 && configStr == string(btes) {
|
||||
return
|
||||
// 配置无变化,跳过写入
|
||||
} else {
|
||||
configNoteStr := ConfigNote.ToJsonString()
|
||||
_ = os.MkdirAll(filepath.Dir(that.configPath), os.ModeDir)
|
||||
err = ioutil.WriteFile(that.configPath, []byte(configStr), os.ModePerm)
|
||||
if err != nil {
|
||||
that.Log.Error().Err(err).Msg("写入配置文件失败")
|
||||
configErr = true
|
||||
}
|
||||
_ = ioutil.WriteFile(filepath.Dir(that.configPath)+"/configNote.json", []byte(configNoteStr), os.ModePerm)
|
||||
}
|
||||
//写入配置说明
|
||||
//var configNoteByte bytes.Buffer
|
||||
configNoteStr := ConfigNote.ToJsonString()
|
||||
//_ = json.Indent(&configNoteByte, []byte(ConfigNote.ToJsonString()), "", "\t")
|
||||
|
||||
_ = os.MkdirAll(filepath.Dir(that.configPath), os.ModeDir)
|
||||
err = ioutil.WriteFile(that.configPath, []byte(configStr), os.ModePerm)
|
||||
if err != nil {
|
||||
that.Error.SetError(err)
|
||||
}
|
||||
_ = ioutil.WriteFile(filepath.Dir(that.configPath)+"/configNote.json", []byte(configNoteStr), os.ModePerm)
|
||||
|
||||
}
|
||||
|
||||
that.Log = GetLog(that.Config.GetString("logFile"), true)
|
||||
that.Error = Error{Logger: that.Log}
|
||||
logLevel := that.Config.GetCeilInt("logLevel")
|
||||
logFile := that.Config.GetString("logFile")
|
||||
logHistory := that.Config.GetCeilInt("logHistory")
|
||||
if logHistory == 0 {
|
||||
logHistory = 100
|
||||
}
|
||||
that.Log = log.NewLogger(logLevel, logFile, logHistory)
|
||||
that.Db.Log = that.Log
|
||||
if that.Config.Get("webConnectLogShow") == nil || that.Config.GetBool("webConnectLogShow") {
|
||||
that.WebConnectLog = GetLog(that.Config.GetString("webConnectLogFile"), false)
|
||||
that.WebConnectLog = log.NewLogger(1, that.Config.GetString("webConnectLogFile"), 0)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -377,9 +362,12 @@ func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
|
||||
ipStr = Substr(context.Req.RemoteAddr, 0, strings.Index(context.Req.RemoteAddr, ":"))
|
||||
}
|
||||
|
||||
that.WebConnectLog.Infoln(ipStr, context.Req.Method,
|
||||
"time cost:", ObjToFloat64(time.Now().UnixNano()-nowUnixTime.UnixNano())/1000000.00, "ms",
|
||||
"data length:", ObjToFloat64(context.DataSize)/1000.00, "KB", context.HandlerStr)
|
||||
that.WebConnectLog.Info().
|
||||
Str("ip", ipStr).
|
||||
Str("method", context.Req.Method).
|
||||
Float64("cost_ms", ObjToFloat64(time.Now().UnixNano()-nowUnixTime.UnixNano())/1000000.00).
|
||||
Float64("size_kb", ObjToFloat64(context.DataSize)/1000.00).
|
||||
Msg(context.HandlerStr)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -441,7 +429,7 @@ func (that *Application) handler(w http.ResponseWriter, req *http.Request) {
|
||||
|
||||
//设置header
|
||||
delete(header, "Content-Type")
|
||||
if that.Config.GetInt("mode") == 0 {
|
||||
if that.Config.GetCeilInt("logLevel") == 0 {
|
||||
header.Set("Cache-Control", "public")
|
||||
} else {
|
||||
header.Set("Cache-Control", "no-cache")
|
||||
@@ -580,7 +568,7 @@ func Init(config string) *Application {
|
||||
codeMake["name"] = codeMake.GetString("table")
|
||||
}
|
||||
|
||||
appIns.MakeCodeRouter[codeMake.GetString("name")] = &code.MakeCode{Error: appIns.Error}
|
||||
appIns.MakeCodeRouter[codeMake.GetString("name")] = &code.MakeCode{Log: appIns.Log}
|
||||
appIns.MakeCodeRouter[codeMake.GetString("name")].Db2JSON(&appIns.Db, codeMake)
|
||||
|
||||
//接入动态代码层
|
||||
@@ -644,45 +632,39 @@ func SetMysqlDB(appIns *Application, config Map) {
|
||||
appIns.Db.DBName = config.GetString("name")
|
||||
appIns.Db.Prefix = config.GetString("prefix")
|
||||
appIns.Db.Log = appIns.Log
|
||||
appIns.Db.Mode = appIns.Config.GetCeilInt("mode")
|
||||
appIns.SetConnectDB(func(err ...*Error) (master, slave *sql.DB) {
|
||||
//master数据库配置
|
||||
appIns.SetConnectDB(func() (master, slave *sql.DB) {
|
||||
query := config.GetString("user") + ":" + config.GetString("password") +
|
||||
"@tcp(" + config.GetString("host") + ":" + config.GetString("port") + ")/" + config.GetString("name") + "?charset=utf8"
|
||||
DB, e := sql.Open("mysql", query)
|
||||
if e != nil && len(err) != 0 {
|
||||
err[0].SetError(e)
|
||||
if e != nil {
|
||||
appIns.Log.Error().Err(e).Msg("MySQL 主库连接失败")
|
||||
}
|
||||
master = DB
|
||||
//slave数据库配置
|
||||
configSlave := config.GetMap("slave")
|
||||
if configSlave != nil {
|
||||
query := configSlave.GetString("user") + ":" + configSlave.GetString("password") +
|
||||
"@tcp(" + config.GetString("host") + ":" + configSlave.GetString("port") + ")/" + configSlave.GetString("name") + "?charset=utf8"
|
||||
DB1, e := sql.Open("mysql", query)
|
||||
if e != nil && len(err) != 0 {
|
||||
err[0].SetError(e)
|
||||
if e != nil {
|
||||
appIns.Log.Error().Err(e).Msg("MySQL 从库连接失败")
|
||||
}
|
||||
slave = DB1
|
||||
}
|
||||
|
||||
return master, slave
|
||||
//return DB
|
||||
})
|
||||
}
|
||||
func SetSqliteDB(appIns *Application, config Map) {
|
||||
|
||||
appIns.Db.Type = "sqlite"
|
||||
appIns.Db.Prefix = config.GetString("prefix")
|
||||
appIns.Db.Mode = appIns.Config.GetCeilInt("mode")
|
||||
appIns.Db.Log = appIns.Log
|
||||
appIns.SetConnectDB(func(err ...*Error) (master, slave *sql.DB) {
|
||||
appIns.SetConnectDB(func() (master, slave *sql.DB) {
|
||||
db, e := sql.Open("sqlite3", config.GetString("path"))
|
||||
if e != nil && len(err) != 0 {
|
||||
err[0].SetError(e)
|
||||
if e != nil {
|
||||
appIns.Log.Error().Err(e).Msg("SQLite 连接失败")
|
||||
}
|
||||
master = db
|
||||
|
||||
return master, slave
|
||||
})
|
||||
}
|
||||
@@ -692,13 +674,12 @@ func SetDmDB(appIns *Application, config Map) {
|
||||
appIns.Db.DBName = config.GetString("name")
|
||||
appIns.Db.Prefix = config.GetString("prefix")
|
||||
appIns.Db.Log = appIns.Log
|
||||
appIns.Db.Mode = appIns.Config.GetCeilInt("mode")
|
||||
appIns.SetConnectDB(func(err ...*Error) (master, slave *sql.DB) {
|
||||
appIns.SetConnectDB(func() (master, slave *sql.DB) {
|
||||
query := "dm://" + config.GetString("user") + ":" + config.GetString("password") +
|
||||
"@" + config.GetString("host") + ":" + config.GetString("port") + "?schema=" + config.GetString("name")
|
||||
DB, e := sql.Open("dm", query)
|
||||
if e != nil && len(err) != 0 {
|
||||
err[0].SetError(e)
|
||||
if e != nil {
|
||||
appIns.Log.Error().Err(e).Msg("达梦主库连接失败")
|
||||
}
|
||||
master = DB
|
||||
configSlave := config.GetMap("slave")
|
||||
@@ -706,8 +687,8 @@ func SetDmDB(appIns *Application, config Map) {
|
||||
querySlave := "dm://" + configSlave.GetString("user") + ":" + configSlave.GetString("password") +
|
||||
"@" + configSlave.GetString("host") + ":" + configSlave.GetString("port") + "?schema=" + configSlave.GetString("name")
|
||||
DB1, e := sql.Open("dm", querySlave)
|
||||
if e != nil && len(err) != 0 {
|
||||
err[0].SetError(e)
|
||||
if e != nil {
|
||||
appIns.Log.Error().Err(e).Msg("达梦从库连接失败")
|
||||
}
|
||||
slave = DB1
|
||||
}
|
||||
|
||||
Vendored
+12
-35
@@ -1,15 +1,14 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
"code.hoteas.com/golang/hotime/log"
|
||||
)
|
||||
|
||||
// HoTimeCache 可配置memory,db,redis,默认启用memory,默认优先级为memory>redis>db,memory与数据库缓存设置项一致,
|
||||
// 缓存数据填充会自动反方向反哺,加入memory缓存过期将自动从redis更新,但memory永远不会更新redis,如果是集群建议不要开启memory,配置即启用
|
||||
type HoTimeCache struct {
|
||||
*Error
|
||||
Log *log.Logger
|
||||
dbCache *CacheDb
|
||||
redisCache *CacheRedis
|
||||
memoryCache *CacheMemory
|
||||
@@ -383,16 +382,13 @@ func (that *HoTimeCache) CachesDelete(keys []string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (that *HoTimeCache) Init(config Map, hotimeDb HoTimeDBInterface, err ...*Error) {
|
||||
//防止空数据问题
|
||||
func (that *HoTimeCache) Init(config Map, hotimeDb HoTimeDBInterface, logger *log.Logger) {
|
||||
if config == nil {
|
||||
config = Map{}
|
||||
}
|
||||
if err[0] != nil {
|
||||
that.Error = err[0]
|
||||
}
|
||||
that.Log = logger
|
||||
that.Config = config
|
||||
//memory配置初始化
|
||||
|
||||
memory := that.Config.GetMap("memory")
|
||||
if memory == nil {
|
||||
memory = Map{
|
||||
@@ -401,71 +397,56 @@ func (that *HoTimeCache) Init(config Map, hotimeDb HoTimeDBInterface, err ...*Er
|
||||
"sort": 0,
|
||||
"timeout": 60 * 60 * 2,
|
||||
}
|
||||
|
||||
}
|
||||
if memory.Get("db") == nil {
|
||||
memory["db"] = true
|
||||
}
|
||||
|
||||
if memory.Get("session") == nil {
|
||||
memory["session"] = true
|
||||
}
|
||||
|
||||
if memory.Get("timeout") == nil {
|
||||
memory["timeout"] = 60 * 60 * 2
|
||||
}
|
||||
that.Config["memory"] = memory
|
||||
that.memoryCache = &CacheMemory{TimeOut: memory.GetCeilInt64("timeout"),
|
||||
DbSet: memory.GetBool("db"), SessionSet: memory.GetBool("session")}
|
||||
if err[0] != nil {
|
||||
that.memoryCache.SetError(err[0])
|
||||
}
|
||||
DbSet: memory.GetBool("db"), SessionSet: memory.GetBool("session"),
|
||||
Log: logger}
|
||||
|
||||
//db配置初始化
|
||||
redis := that.Config.GetMap("redis")
|
||||
if redis != nil {
|
||||
if redis.GetString("host") == "" || redis.GetString("port") == "" {
|
||||
if err[0] != nil {
|
||||
err[0].SetError(errors.New("请检查redis配置host和port配置"))
|
||||
if logger != nil {
|
||||
logger.Error().Msg("请检查redis配置host和port配置")
|
||||
}
|
||||
return
|
||||
}
|
||||
if redis.Get("db") == nil {
|
||||
redis["db"] = true
|
||||
}
|
||||
|
||||
if redis.Get("session") == nil {
|
||||
redis["session"] = true
|
||||
}
|
||||
|
||||
if redis.Get("timeout") == nil {
|
||||
redis["timeout"] = 60 * 60 * 24 * 15
|
||||
}
|
||||
that.Config["redis"] = redis
|
||||
that.redisCache = &CacheRedis{TimeOut: redis.GetCeilInt64("timeout"),
|
||||
DbSet: redis.GetBool("db"), SessionSet: redis.GetBool("session"), Host: redis.GetString("host"),
|
||||
Pwd: redis.GetString("password"), Port: redis.GetCeilInt64("port")}
|
||||
|
||||
if err[0] != nil {
|
||||
that.redisCache.SetError(err[0])
|
||||
}
|
||||
Pwd: redis.GetString("password"), Port: redis.GetCeilInt64("port"),
|
||||
Log: logger}
|
||||
}
|
||||
|
||||
//db配置初始化
|
||||
db := that.Config.GetMap("db")
|
||||
if db != nil {
|
||||
|
||||
if db.Get("db") == nil {
|
||||
db["db"] = false
|
||||
}
|
||||
|
||||
if db.Get("session") == nil {
|
||||
db["session"] = true
|
||||
}
|
||||
if db.Get("timeout") == nil {
|
||||
db["timeout"] = 60 * 60 * 24 * 30
|
||||
}
|
||||
// mode 默认为 "compatible"(兼容模式,便于老系统平滑升级)
|
||||
if db.Get("mode") == nil {
|
||||
db["mode"] = CacheModeCompatible
|
||||
}
|
||||
@@ -478,13 +459,9 @@ func (that *HoTimeCache) Init(config Map, hotimeDb HoTimeDBInterface, err ...*Er
|
||||
HistorySet: db.GetBool("history"),
|
||||
Mode: db.GetString("mode"),
|
||||
Db: hotimeDb,
|
||||
}
|
||||
|
||||
if err[0] != nil {
|
||||
that.dbCache.SetError(err[0])
|
||||
Log: logger,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (that *HoTimeCache) DisableDbCache() {
|
||||
|
||||
Vendored
+3
-10
@@ -7,6 +7,7 @@ import (
|
||||
"time"
|
||||
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
"code.hoteas.com/golang/hotime/log"
|
||||
)
|
||||
|
||||
// 表名常量
|
||||
@@ -22,7 +23,7 @@ const (
|
||||
type HoTimeDBInterface interface {
|
||||
GetPrefix() string
|
||||
Query(query string, args ...interface{}) []Map
|
||||
Exec(query string, args ...interface{}) (sql.Result, *Error)
|
||||
Exec(query string, args ...interface{}) (sql.Result, error)
|
||||
Get(table string, qu ...interface{}) Map
|
||||
Select(table string, qu ...interface{}) []Map
|
||||
Delete(table string, data map[string]interface{}) int64
|
||||
@@ -38,19 +39,11 @@ type CacheDb struct {
|
||||
HistorySet bool // 是否开启历史记录
|
||||
Mode string // 缓存模式:"new"(默认,只用新表) 或 "compatible"(写新读老)
|
||||
Db HoTimeDBInterface
|
||||
*Error
|
||||
Log *log.Logger
|
||||
ContextBase
|
||||
isInit bool
|
||||
}
|
||||
|
||||
func (that *CacheDb) GetError() *Error {
|
||||
return that.Error
|
||||
}
|
||||
|
||||
func (that *CacheDb) SetError(err *Error) {
|
||||
that.Error = err
|
||||
}
|
||||
|
||||
// getTableName 获取带前缀的表名
|
||||
func (that *CacheDb) getTableName() string {
|
||||
return that.Db.GetPrefix() + CacheTableName
|
||||
|
||||
Vendored
+4
-15
@@ -2,6 +2,7 @@ package cache
|
||||
|
||||
import (
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
"code.hoteas.com/golang/hotime/log"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -12,24 +13,12 @@ type CacheMemory struct {
|
||||
TimeOut int64
|
||||
DbSet bool
|
||||
SessionSet bool
|
||||
*Error
|
||||
cache sync.Map // 替代传统的 Map
|
||||
}
|
||||
|
||||
func (that *CacheMemory) GetError() *Error {
|
||||
|
||||
return that.Error
|
||||
|
||||
}
|
||||
|
||||
func (that *CacheMemory) SetError(err *Error) {
|
||||
that.Error = err
|
||||
Log *log.Logger
|
||||
cache sync.Map
|
||||
}
|
||||
func (c *CacheMemory) get(key string) (res *Obj) {
|
||||
|
||||
res = &Obj{
|
||||
Error: *c.Error,
|
||||
}
|
||||
res = &Obj{}
|
||||
value, ok := c.cache.Load(key)
|
||||
if !ok {
|
||||
return res // 缓存不存在
|
||||
|
||||
Vendored
+16
-20
@@ -2,6 +2,7 @@ package cache
|
||||
|
||||
import (
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
"code.hoteas.com/golang/hotime/log"
|
||||
"github.com/garyburd/redigo/redis"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -18,18 +19,14 @@ type CacheRedis struct {
|
||||
pool *redis.Pool
|
||||
tag int64
|
||||
ContextBase
|
||||
*Error
|
||||
Log *log.Logger
|
||||
initOnce sync.Once
|
||||
}
|
||||
|
||||
func (that *CacheRedis) GetError() *Error {
|
||||
|
||||
return that.Error
|
||||
|
||||
}
|
||||
|
||||
func (that *CacheRedis) SetError(err *Error) {
|
||||
that.Error = err
|
||||
func (that *CacheRedis) logErr(err error) {
|
||||
if err != nil && that.Log != nil {
|
||||
that.Log.Error().Err(err).Msg("redis cache error")
|
||||
}
|
||||
}
|
||||
|
||||
// 唯一标志
|
||||
@@ -97,7 +94,7 @@ func (that *CacheRedis) del(key string) {
|
||||
if del != -1 {
|
||||
val, err := redis.Strings(conn.Do("KEYS", key))
|
||||
if err != nil {
|
||||
that.Error.SetError(err)
|
||||
that.logErr(err)
|
||||
return
|
||||
}
|
||||
if len(val) == 0 {
|
||||
@@ -109,12 +106,12 @@ func (that *CacheRedis) del(key string) {
|
||||
}
|
||||
_, err = conn.Do("EXEC")
|
||||
if err != nil {
|
||||
that.Error.SetError(err)
|
||||
that.logErr(err)
|
||||
}
|
||||
} else {
|
||||
_, err := conn.Do("DEL", key)
|
||||
if err != nil {
|
||||
that.Error.SetError(err)
|
||||
that.logErr(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +126,7 @@ func (that *CacheRedis) set(key string, value string, expireSeconds int64) {
|
||||
|
||||
_, err := conn.Do("SET", key, value, "EX", ObjToStr(expireSeconds))
|
||||
if err != nil {
|
||||
that.Error.SetError(err)
|
||||
that.logErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,7 +143,7 @@ func (that *CacheRedis) get(key string) *Obj {
|
||||
if err != nil {
|
||||
reData.Data = nil
|
||||
if !strings.Contains(err.Error(), "nil returned") {
|
||||
that.Error.SetError(err)
|
||||
that.logErr(err)
|
||||
}
|
||||
}
|
||||
return reData
|
||||
@@ -175,11 +172,10 @@ func (that *CacheRedis) Cache(key string, data ...interface{}) *Obj {
|
||||
tim += that.TimeOut
|
||||
}
|
||||
if len(data) == 2 {
|
||||
that.Error.SetError(nil)
|
||||
tempt := ObjToInt64(data[1], that.Error)
|
||||
tempt := ObjToInt64(data[1])
|
||||
if tempt > tim {
|
||||
tim = tempt
|
||||
} else if that.GetError() == nil {
|
||||
} else {
|
||||
tim = tim + tempt
|
||||
}
|
||||
}
|
||||
@@ -212,7 +208,7 @@ func (that *CacheRedis) CachesGet(keys []string) Map {
|
||||
values, err := redis.Strings(conn.Do("MGET", args...))
|
||||
if err != nil {
|
||||
if !strings.Contains(err.Error(), "nil returned") {
|
||||
that.Error.SetError(err)
|
||||
that.logErr(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
@@ -257,7 +253,7 @@ func (that *CacheRedis) CachesSet(data Map, timeout ...int64) {
|
||||
}
|
||||
_, err := conn.Do("EXEC")
|
||||
if err != nil {
|
||||
that.Error.SetError(err)
|
||||
that.logErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -281,6 +277,6 @@ func (that *CacheRedis) CachesDelete(keys []string) {
|
||||
|
||||
_, err := conn.Do("DEL", args...)
|
||||
if err != nil {
|
||||
that.Error.SetError(err)
|
||||
that.logErr(err)
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+3
-9
@@ -5,16 +5,10 @@ import (
|
||||
)
|
||||
|
||||
type CacheIns interface {
|
||||
//set(key string, value interface{}, time int64)
|
||||
//get(key string) interface{}
|
||||
//delete(key string)
|
||||
GetError() *Error
|
||||
SetError(err *Error)
|
||||
Cache(key string, data ...interface{}) *Obj
|
||||
// 批量操作
|
||||
CachesGet(keys []string) Map // 批量获取
|
||||
CachesSet(data Map, timeout ...int64) // 批量设置
|
||||
CachesDelete(keys []string) // 批量删除
|
||||
CachesGet(keys []string) Map
|
||||
CachesSet(data Map, timeout ...int64)
|
||||
CachesDelete(keys []string)
|
||||
}
|
||||
|
||||
// 单条缓存数据
|
||||
|
||||
+11
-11
@@ -3,7 +3,7 @@ package code
|
||||
import (
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
"code.hoteas.com/golang/hotime/db"
|
||||
"errors"
|
||||
"code.hoteas.com/golang/hotime/log"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
@@ -22,7 +22,7 @@ type MakeCode struct {
|
||||
SearchColumns map[string]map[string]Map
|
||||
Config Map
|
||||
RuleConfig []Map
|
||||
Error
|
||||
Log *log.Logger
|
||||
}
|
||||
|
||||
func (that *MakeCode) Db2JSON(db *db.HoTimeDB, config Map) {
|
||||
@@ -49,7 +49,7 @@ func (that *MakeCode) Db2JSON(db *db.HoTimeDB, config Map) {
|
||||
if errRule == nil {
|
||||
//cmap := Map{}
|
||||
//文件是否损坏
|
||||
ruleLis := ObjToSlice(string(btesRule), &that.Error)
|
||||
ruleLis := ObjToSlice(string(btesRule))
|
||||
//cmap.JSON()
|
||||
for k, _ := range ruleLis {
|
||||
that.RuleConfig = append(that.RuleConfig, ruleLis.GetMap(k))
|
||||
@@ -62,11 +62,11 @@ func (that *MakeCode) Db2JSON(db *db.HoTimeDB, config Map) {
|
||||
_ = os.MkdirAll(filepath.Dir(config.GetString("rule")), os.ModeDir)
|
||||
err = ioutil.WriteFile(config.GetString("rule"), []byte(ObjToStr(that.RuleConfig)), os.ModePerm)
|
||||
if err != nil {
|
||||
that.Error.SetError(err)
|
||||
that.Log.Error().Err(err).Msg("写入 rule 配置文件失败")
|
||||
}
|
||||
}
|
||||
|
||||
that.Error.SetError(errors.New("rule配置文件不存在,或者配置出错,使用缺省默认配置"))
|
||||
that.Log.Warnf("rule配置文件不存在或配置出错,使用缺省默认配置")
|
||||
}
|
||||
|
||||
that.IndexMenus = Map{}
|
||||
@@ -360,7 +360,7 @@ func (that *MakeCode) Db2JSON(db *db.HoTimeDB, config Map) {
|
||||
_ = os.MkdirAll(config.GetString("name"), os.ModeDir)
|
||||
err = ioutil.WriteFile(config.GetString("name")+"/"+v.GetString("name")+".go", []byte(myCtr), os.ModePerm)
|
||||
if err != nil {
|
||||
that.Error.SetError(err)
|
||||
that.Log.Error().Err(err).Msg("写入代码文件失败")
|
||||
}
|
||||
isMake = true
|
||||
}
|
||||
@@ -656,7 +656,7 @@ func (that *MakeCode) Db2JSON(db *db.HoTimeDB, config Map) {
|
||||
_ = os.MkdirAll(config.GetString("name"), os.ModeDir)
|
||||
err = ioutil.WriteFile(config.GetString("name")+"/init.go", []byte(myInit), os.ModePerm)
|
||||
if err != nil {
|
||||
that.Error.SetError(err)
|
||||
that.Log.Error().Err(err).Msg("写入 init.go 失败")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -667,9 +667,9 @@ func (that *MakeCode) Db2JSON(db *db.HoTimeDB, config Map) {
|
||||
_ = os.MkdirAll(filepath.Dir(config.GetString("configDB")), os.ModeDir)
|
||||
err = ioutil.WriteFile(config.GetString("configDB"), []byte(that.Config.ToJsonString()), os.ModePerm)
|
||||
if err != nil {
|
||||
that.Error.SetError(err)
|
||||
that.Log.Error().Err(err).Msg("写入 configDB 文件失败")
|
||||
}
|
||||
that.Logger.Warn("新建")
|
||||
that.Log.Warnf("新建 configDB: %s", config.GetString("configDB"))
|
||||
|
||||
}
|
||||
|
||||
@@ -683,9 +683,9 @@ func (that *MakeCode) Db2JSON(db *db.HoTimeDB, config Map) {
|
||||
delete(newConfig, "tables")
|
||||
err = ioutil.WriteFile(config.GetString("config"), []byte(newConfig.ToJsonString()), os.ModePerm)
|
||||
if err != nil {
|
||||
that.Error.SetError(err)
|
||||
that.Log.Error().Err(err).Msg("写入 config 文件失败")
|
||||
}
|
||||
that.Logger.Warn("新建")
|
||||
that.Log.Warnf("新建 config: %s", config.GetString("config"))
|
||||
|
||||
}
|
||||
//fmt.Println("有新的代码生成,请重新运行")
|
||||
|
||||
+37
-7
@@ -1,29 +1,59 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/sirupsen/logrus"
|
||||
"code.hoteas.com/golang/hotime/log"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Error 框架层处理错误
|
||||
// Error 框架基础错误类型 —— 线程安全的 error 封装。
|
||||
// 通过组合(嵌入)方式在 Obj、DBError 等结构体中使用,提供统一的错误 API。
|
||||
// 也用于 ObjToXxx / Map.GetXxx 等工具函数的可选错误报告参数。
|
||||
// 设置 Logger 后,SetError 会自动将非 nil 错误写入日志。
|
||||
type Error struct {
|
||||
Logger *logrus.Logger
|
||||
error
|
||||
mu sync.RWMutex
|
||||
mu sync.RWMutex
|
||||
Logger *log.Logger
|
||||
}
|
||||
|
||||
// Error 安全实现 error 接口 —— 内部 error 为 nil 时返回 "" 而非 panic
|
||||
func (that *Error) Error() string {
|
||||
that.mu.RLock()
|
||||
defer that.mu.RUnlock()
|
||||
if that.error == nil {
|
||||
return ""
|
||||
}
|
||||
return that.error.Error()
|
||||
}
|
||||
|
||||
// HasError 快捷判断是否存在错误
|
||||
func (that *Error) HasError() bool {
|
||||
that.mu.RLock()
|
||||
defer that.mu.RUnlock()
|
||||
return that.error != nil
|
||||
}
|
||||
|
||||
// Unwrap 支持 errors.Is / errors.As 标准错误链
|
||||
func (that *Error) Unwrap() error {
|
||||
that.mu.RLock()
|
||||
defer that.mu.RUnlock()
|
||||
return that.error
|
||||
}
|
||||
|
||||
// GetError 获取内部 error(nil 表示无错误)
|
||||
func (that *Error) GetError() error {
|
||||
that.mu.RLock()
|
||||
defer that.mu.RUnlock()
|
||||
return that.error
|
||||
}
|
||||
|
||||
// SetError 设置内部 error(传 nil 清除错误)
|
||||
// 当 Logger 已设置且 err 非 nil 时,自动写入 ERROR 级别日志
|
||||
func (that *Error) SetError(err error) {
|
||||
that.mu.Lock()
|
||||
that.error = err
|
||||
logger := that.Logger
|
||||
that.mu.Unlock()
|
||||
|
||||
if that.Logger != nil && err != nil {
|
||||
that.Logger.Warn(err)
|
||||
if err != nil && logger != nil {
|
||||
logger.Error().Err(err).Msg("")
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -3,7 +3,6 @@ package hotime
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
@@ -61,7 +60,7 @@ func (that *Context) Display(statu int, data interface{}) {
|
||||
//兼容android等需要json转对象的服务
|
||||
resp["error"] = temp
|
||||
|
||||
that.Error.SetError(errors.New(resp.ToJsonString()))
|
||||
that.Application.Log.Warn().Int("status", statu).Msg(resp.ToJsonString())
|
||||
|
||||
} else {
|
||||
resp["result"] = data
|
||||
|
||||
+21
-13
@@ -8,6 +8,10 @@ import (
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
)
|
||||
|
||||
func (that *HoTimeDB) isCacheTable(table string) bool {
|
||||
return table == "cached" || strings.HasPrefix(table, "hotime_cache")
|
||||
}
|
||||
|
||||
// Page 设置分页参数
|
||||
// page: 页码(从1开始)
|
||||
// pageRow: 每页数量
|
||||
@@ -135,7 +139,7 @@ func (that *HoTimeDB) Select(table string, qu ...interface{}) []Map {
|
||||
qs = append(qs, resWhere...)
|
||||
md5 := that.md5(query, qs...)
|
||||
|
||||
if that.testTx == nil && that.HoTimeCache != nil && table != "cached" {
|
||||
if that.testTx == nil && that.HoTimeCache != nil && !that.isCacheTable(table) {
|
||||
cacheData := that.HoTimeCache.Db(table + ":" + md5)
|
||||
if cacheData != nil && cacheData.Data != nil {
|
||||
return cacheData.ToMapArray()
|
||||
@@ -148,7 +152,7 @@ func (that *HoTimeDB) Select(table string, qu ...interface{}) []Map {
|
||||
res = []Map{}
|
||||
}
|
||||
|
||||
if that.testTx == nil && that.HoTimeCache != nil && table != "cached" {
|
||||
if that.testTx == nil && that.HoTimeCache != nil && !that.isCacheTable(table) {
|
||||
_ = that.HoTimeCache.Db(table+":"+md5, res)
|
||||
}
|
||||
|
||||
@@ -295,16 +299,20 @@ func (that *HoTimeDB) Insert(table string, data map[string]interface{}) int64 {
|
||||
}
|
||||
} else {
|
||||
res, err := that.Exec(query, values...)
|
||||
if err.GetError() == nil && res != nil {
|
||||
if err == nil && res != nil {
|
||||
id1, e := res.LastInsertId()
|
||||
that.LastErr.SetError(e)
|
||||
if e != nil {
|
||||
dbErr := &DBError{}
|
||||
dbErr.SetError(e)
|
||||
that.lastError.Store(dbErr)
|
||||
}
|
||||
id = id1
|
||||
}
|
||||
}
|
||||
|
||||
// 如果插入成功,删除缓存
|
||||
if id != 0 {
|
||||
if that.HoTimeCache != nil && table != "cached" {
|
||||
if that.HoTimeCache != nil && !that.isCacheTable(table) {
|
||||
_ = that.HoTimeCache.Db(table+"*", nil)
|
||||
}
|
||||
}
|
||||
@@ -386,13 +394,13 @@ func (that *HoTimeDB) Inserts(table string, dataList []Map) int64 {
|
||||
res, err := that.Exec(query, values...)
|
||||
|
||||
rows64 := int64(0)
|
||||
if err.GetError() == nil && res != nil {
|
||||
if err == nil && res != nil {
|
||||
rows64, _ = res.RowsAffected()
|
||||
}
|
||||
|
||||
// 如果插入成功,删除缓存
|
||||
if rows64 != 0 {
|
||||
if that.HoTimeCache != nil && table != "cached" {
|
||||
if that.HoTimeCache != nil && !that.isCacheTable(table) {
|
||||
_ = that.HoTimeCache.Db(table+"*", nil)
|
||||
}
|
||||
}
|
||||
@@ -492,13 +500,13 @@ func (that *HoTimeDB) Upsert(table string, data Map, uniqueKeys Slice, updateCol
|
||||
res, err := that.Exec(query, values...)
|
||||
|
||||
rows := int64(0)
|
||||
if err.GetError() == nil && res != nil {
|
||||
if err == nil && res != nil {
|
||||
rows, _ = res.RowsAffected()
|
||||
}
|
||||
|
||||
// 清除缓存
|
||||
if rows != 0 {
|
||||
if that.HoTimeCache != nil && table != "cached" {
|
||||
if that.HoTimeCache != nil && !that.isCacheTable(table) {
|
||||
_ = that.HoTimeCache.Db(table+"*", nil)
|
||||
}
|
||||
}
|
||||
@@ -693,13 +701,13 @@ func (that *HoTimeDB) Update(table string, data Map, where Map) int64 {
|
||||
res, err := that.Exec(query, qs...)
|
||||
|
||||
rows := int64(0)
|
||||
if err.GetError() == nil && res != nil {
|
||||
if err == nil && res != nil {
|
||||
rows, _ = res.RowsAffected()
|
||||
}
|
||||
|
||||
// 如果更新成功,则删除缓存
|
||||
if rows != 0 {
|
||||
if that.HoTimeCache != nil && table != "cached" {
|
||||
if that.HoTimeCache != nil && !that.isCacheTable(table) {
|
||||
_ = that.HoTimeCache.Db(table+"*", nil)
|
||||
}
|
||||
}
|
||||
@@ -717,13 +725,13 @@ func (that *HoTimeDB) Delete(table string, data map[string]interface{}) int64 {
|
||||
|
||||
res, err := that.Exec(query, resWhere...)
|
||||
rows := int64(0)
|
||||
if err.GetError() == nil && res != nil {
|
||||
if err == nil && res != nil {
|
||||
rows, _ = res.RowsAffected()
|
||||
}
|
||||
|
||||
// 如果删除成功,删除对应缓存
|
||||
if rows != 0 {
|
||||
if that.HoTimeCache != nil && table != "cached" {
|
||||
if that.HoTimeCache != nil && !that.isCacheTable(table) {
|
||||
_ = that.HoTimeCache.Db(table+"*", nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,73 +4,114 @@ import (
|
||||
"bytes"
|
||||
"code.hoteas.com/golang/hotime/cache"
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
"code.hoteas.com/golang/hotime/log"
|
||||
"database/sql"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
_ "gitee.com/chunanyong/dm"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// DBError 最后一次 SQL 操作的结构化快照
|
||||
// 嵌入框架 Error 类型,与 Obj 等保持一致的错误 API(HasError/GetError/Unwrap)
|
||||
type DBError struct {
|
||||
Error
|
||||
Query string
|
||||
Data []interface{}
|
||||
}
|
||||
|
||||
// HoTimeDB 数据库操作核心结构体
|
||||
type HoTimeDB struct {
|
||||
*sql.DB
|
||||
ContextBase
|
||||
DBName string
|
||||
*cache.HoTimeCache
|
||||
Log *logrus.Logger
|
||||
Type string // 数据库类型: mysql, sqlite3, postgres
|
||||
Prefix string
|
||||
LastQuery string
|
||||
LastData []interface{}
|
||||
ConnectFunc func(err ...*Error) (*sql.DB, *sql.DB)
|
||||
LastErr *Error
|
||||
limit Slice
|
||||
*sql.Tx //事务对象
|
||||
SlaveDB *sql.DB // 从数据库
|
||||
Mode int // mode为0生产模式,1为测试模式,2为开发模式
|
||||
mu sync.RWMutex
|
||||
limitMu sync.Mutex
|
||||
Dialect Dialect // 数据库方言适配器
|
||||
testTx *sql.Tx // 测试事务:设置后所有操作都在此事务内,测试结束回滚
|
||||
testMu *sync.Mutex // 保护 testTx 单连接不被并发访问
|
||||
testLogBuf *bytes.Buffer // 测试模式:SQL 日志写入此 buffer 而非直接打印,失败时才输出
|
||||
testLogBufMu sync.Mutex // 保护 testLogBuf 并发写入
|
||||
txFailed *bool // 事务内是否有SQL出错,出错则事务必须回滚(指针确保按值传递 HoTimeDB 时状态共享)
|
||||
Log *log.Logger
|
||||
Type string // 数据库类型: mysql, sqlite3, postgres
|
||||
Prefix string
|
||||
ConnectFunc func() (*sql.DB, *sql.DB)
|
||||
lastError atomic.Pointer[DBError] // 无锁读写,始终记录最后一次 SQL 操作
|
||||
limit Slice
|
||||
*sql.Tx //事务对象
|
||||
SlaveDB *sql.DB // 从数据库
|
||||
limitMu sync.Mutex
|
||||
Dialect Dialect // 数据库方言适配器
|
||||
testTx *sql.Tx // 测试事务:设置后所有操作都在此事务内,测试结束回滚
|
||||
testMu *sync.Mutex // 保护 testTx 单连接不被并发访问
|
||||
testLogBuf *bytes.Buffer // 测试模式:SQL 日志写入此 buffer 而非直接打印,失败时才输出
|
||||
testLogBufMu sync.Mutex // 保护 testLogBuf 并发写入
|
||||
txFailed *bool // 事务内是否有SQL出错,出错则事务必须回滚(指针确保按值传递 HoTimeDB 时状态共享)
|
||||
}
|
||||
|
||||
// GetLast 返回最后一次 SQL 操作的完整快照(Query + Data + Error)
|
||||
func (that *HoTimeDB) GetLast() *DBError {
|
||||
return that.lastError.Load()
|
||||
}
|
||||
|
||||
// GetLastError 返回最后一次 SQL 的错误(nil = 成功)
|
||||
func (that *HoTimeDB) GetLastError() error {
|
||||
if e := that.lastError.Load(); e != nil {
|
||||
return e.GetError()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetLastQuery 返回最后一次执行的 SQL 语句
|
||||
func (that *HoTimeDB) GetLastQuery() string {
|
||||
if e := that.lastError.Load(); e != nil {
|
||||
return e.Query
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// GetLastData 返回最后一次 SQL 的参数
|
||||
func (that *HoTimeDB) GetLastData() []interface{} {
|
||||
if e := that.lastError.Load(); e != nil {
|
||||
return e.Data
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetConnect 设置数据库配置连接
|
||||
func (that *HoTimeDB) SetConnect(connect func(err ...*Error) (master, slave *sql.DB), err ...*Error) {
|
||||
func (that *HoTimeDB) SetConnect(connect func() (master, slave *sql.DB)) {
|
||||
that.ConnectFunc = connect
|
||||
_ = that.InitDb(err...)
|
||||
that.InitDb()
|
||||
}
|
||||
|
||||
// InitDb 初始化数据库连接
|
||||
func (that *HoTimeDB) InitDb(err ...*Error) *Error {
|
||||
if len(err) != 0 {
|
||||
that.LastErr = err[0]
|
||||
}
|
||||
that.DB, that.SlaveDB = that.ConnectFunc(that.LastErr)
|
||||
func (that *HoTimeDB) InitDb() {
|
||||
that.DB, that.SlaveDB = that.ConnectFunc()
|
||||
if that.DB == nil {
|
||||
return that.LastErr
|
||||
return
|
||||
}
|
||||
e := that.DB.Ping()
|
||||
|
||||
that.LastErr.SetError(e)
|
||||
if e != nil {
|
||||
dbErr := &DBError{}
|
||||
dbErr.SetError(e)
|
||||
that.lastError.Store(dbErr)
|
||||
if that.Log != nil {
|
||||
that.Log.Error().Err(e).Msg("数据库连接失败")
|
||||
}
|
||||
}
|
||||
|
||||
if that.SlaveDB != nil {
|
||||
e := that.SlaveDB.Ping()
|
||||
that.LastErr.SetError(e)
|
||||
if e != nil {
|
||||
dbErr := &DBError{}
|
||||
dbErr.SetError(e)
|
||||
that.lastError.Store(dbErr)
|
||||
if that.Log != nil {
|
||||
that.Log.Error().Err(e).Msg("从数据库连接失败")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 根据数据库类型初始化方言适配器
|
||||
if that.Dialect == nil {
|
||||
that.initDialect()
|
||||
}
|
||||
|
||||
return that.LastErr
|
||||
}
|
||||
|
||||
// initDialect 根据数据库类型初始化方言
|
||||
|
||||
+65
-73
@@ -28,55 +28,37 @@ func (that *HoTimeDB) Query(query string, args ...interface{}) []Map {
|
||||
|
||||
// queryWithRetry 内部查询方法,支持重试标记
|
||||
func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interface{}) []Map {
|
||||
// 预处理数组占位符 ?[]
|
||||
query, args = that.expandArrayPlaceholder(query, args)
|
||||
|
||||
// 保存调试信息(加锁保护)
|
||||
that.mu.Lock()
|
||||
that.LastQuery = query
|
||||
that.LastData = args
|
||||
that.mu.Unlock()
|
||||
var sqlErr error
|
||||
|
||||
defer func() {
|
||||
if that.Mode != 0 {
|
||||
that.mu.RLock()
|
||||
msg := fmt.Sprintf("SQL:%s DATA:%v ERROR:%v", that.LastQuery, that.LastData, that.LastErr.GetError())
|
||||
that.mu.RUnlock()
|
||||
that.testLogBufMu.Lock()
|
||||
if that.testLogBuf != nil {
|
||||
that.testLogBuf.WriteString(msg + "\n")
|
||||
} else {
|
||||
that.Log.Info(msg)
|
||||
}
|
||||
that.testLogBufMu.Unlock()
|
||||
}
|
||||
e := &DBError{Query: query, Data: args}
|
||||
e.SetError(sqlErr)
|
||||
that.lastError.Store(e)
|
||||
that.logSQL(query, args, sqlErr)
|
||||
}()
|
||||
|
||||
var err error
|
||||
var resl *sql.Rows
|
||||
|
||||
// 主从数据库切换,只有select语句有从数据库
|
||||
db := that.DB
|
||||
if that.SlaveDB != nil {
|
||||
db = that.SlaveDB
|
||||
}
|
||||
|
||||
if db == nil {
|
||||
err = errors.New("没有初始化数据库")
|
||||
that.LastErr.SetError(err)
|
||||
sqlErr = errors.New("没有初始化数据库")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理参数中的 slice 类型
|
||||
processedArgs := that.processArgs(args)
|
||||
|
||||
if that.testTx != nil {
|
||||
if that.testMu != nil {
|
||||
that.testMu.Lock()
|
||||
}
|
||||
resl, err = that.testTx.Query(query, processedArgs...)
|
||||
that.LastErr.SetError(err)
|
||||
if err != nil {
|
||||
resl, sqlErr = that.testTx.Query(query, processedArgs...)
|
||||
if sqlErr != nil {
|
||||
if that.txFailed != nil {
|
||||
*that.txFailed = true
|
||||
}
|
||||
@@ -91,13 +73,12 @@ func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interfa
|
||||
}
|
||||
return result
|
||||
} else if that.Tx != nil {
|
||||
resl, err = that.Tx.Query(query, processedArgs...)
|
||||
resl, sqlErr = that.Tx.Query(query, processedArgs...)
|
||||
} else {
|
||||
resl, err = db.Query(query, processedArgs...)
|
||||
resl, sqlErr = db.Query(query, processedArgs...)
|
||||
}
|
||||
|
||||
that.LastErr.SetError(err)
|
||||
if err != nil {
|
||||
if sqlErr != nil {
|
||||
if that.Tx != nil {
|
||||
if that.txFailed != nil {
|
||||
*that.txFailed = true
|
||||
@@ -105,6 +86,7 @@ func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interfa
|
||||
}
|
||||
if !retried && that.Tx == nil {
|
||||
if pingErr := db.Ping(); pingErr == nil {
|
||||
sqlErr = nil // 重置,让 retry 的 defer 覆盖
|
||||
return that.queryWithRetry(query, true, args...)
|
||||
}
|
||||
}
|
||||
@@ -115,88 +97,94 @@ func (that *HoTimeDB) queryWithRetry(query string, retried bool, args ...interfa
|
||||
}
|
||||
|
||||
// Exec 执行非查询 SQL
|
||||
func (that *HoTimeDB) Exec(query string, args ...interface{}) (sql.Result, *Error) {
|
||||
func (that *HoTimeDB) Exec(query string, args ...interface{}) (sql.Result, error) {
|
||||
return that.execWithRetry(query, false, args...)
|
||||
}
|
||||
|
||||
// execWithRetry 内部执行方法,支持重试标记
|
||||
func (that *HoTimeDB) execWithRetry(query string, retried bool, args ...interface{}) (sql.Result, *Error) {
|
||||
// 预处理数组占位符 ?[]
|
||||
func (that *HoTimeDB) execWithRetry(query string, retried bool, args ...interface{}) (sql.Result, error) {
|
||||
query, args = that.expandArrayPlaceholder(query, args)
|
||||
|
||||
// 保存调试信息(加锁保护)
|
||||
that.mu.Lock()
|
||||
that.LastQuery = query
|
||||
that.LastData = args
|
||||
that.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
if that.Mode != 0 {
|
||||
that.mu.RLock()
|
||||
msg := fmt.Sprintf("SQL:%s DATA:%v ERROR:%v", that.LastQuery, that.LastData, that.LastErr.GetError())
|
||||
that.mu.RUnlock()
|
||||
that.testLogBufMu.Lock()
|
||||
if that.testLogBuf != nil {
|
||||
that.testLogBuf.WriteString(msg + "\n")
|
||||
} else {
|
||||
that.Log.Info(msg)
|
||||
}
|
||||
that.testLogBufMu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
var e error
|
||||
var sqlErr error
|
||||
var resl sql.Result
|
||||
|
||||
defer func() {
|
||||
e := &DBError{Query: query, Data: args}
|
||||
e.SetError(sqlErr)
|
||||
that.lastError.Store(e)
|
||||
that.logSQL(query, args, sqlErr)
|
||||
}()
|
||||
|
||||
if that.DB == nil {
|
||||
err := errors.New("没有初始化数据库")
|
||||
that.LastErr.SetError(err)
|
||||
return nil, that.LastErr
|
||||
sqlErr = errors.New("没有初始化数据库")
|
||||
return nil, sqlErr
|
||||
}
|
||||
|
||||
// 处理参数中的 slice 类型
|
||||
processedArgs := that.processArgs(args)
|
||||
|
||||
if that.testTx != nil {
|
||||
if that.testMu != nil {
|
||||
that.testMu.Lock()
|
||||
}
|
||||
resl, e = that.testTx.Exec(query, processedArgs...)
|
||||
resl, sqlErr = that.testTx.Exec(query, processedArgs...)
|
||||
if that.testMu != nil {
|
||||
that.testMu.Unlock()
|
||||
}
|
||||
} else if that.Tx != nil {
|
||||
resl, e = that.Tx.Exec(query, processedArgs...)
|
||||
resl, sqlErr = that.Tx.Exec(query, processedArgs...)
|
||||
} else {
|
||||
resl, e = that.DB.Exec(query, processedArgs...)
|
||||
resl, sqlErr = that.DB.Exec(query, processedArgs...)
|
||||
}
|
||||
|
||||
that.LastErr.SetError(e)
|
||||
if e != nil {
|
||||
// testTx 模式下不走连接池重试,避免 busy buffer
|
||||
if sqlErr != nil {
|
||||
if that.testTx != nil {
|
||||
if that.txFailed != nil {
|
||||
*that.txFailed = true
|
||||
}
|
||||
return resl, that.LastErr
|
||||
return resl, sqlErr
|
||||
}
|
||||
// 事务内不做连接级重试:死锁等错误会导致 MySQL 自动回滚事务,
|
||||
// 在已回滚的 Tx 上重试会以 auto-commit 模式执行,造成数据不一致
|
||||
if that.Tx != nil {
|
||||
if that.txFailed != nil {
|
||||
*that.txFailed = true
|
||||
}
|
||||
return resl, that.LastErr
|
||||
return resl, sqlErr
|
||||
}
|
||||
if !retried {
|
||||
if pingErr := that.DB.Ping(); pingErr == nil {
|
||||
sqlErr = nil
|
||||
return that.execWithRetry(query, true, args...)
|
||||
}
|
||||
}
|
||||
return resl, that.LastErr
|
||||
return resl, sqlErr
|
||||
}
|
||||
|
||||
return resl, that.LastErr
|
||||
return resl, sqlErr
|
||||
}
|
||||
|
||||
// logSQL 统一 SQL 日志输出(仅此一处打印,消除重复)
|
||||
func (that *HoTimeDB) logSQL(query string, args []interface{}, err error) {
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("SQL: %s DATA: %v ERROR: %v", query, args, err)
|
||||
that.testLogBufMu.Lock()
|
||||
if that.testLogBuf != nil {
|
||||
that.testLogBuf.WriteString(msg + "\n")
|
||||
} else if that.Log != nil {
|
||||
that.Log.Error().Msg(msg)
|
||||
}
|
||||
that.testLogBufMu.Unlock()
|
||||
return
|
||||
}
|
||||
// 无错误时只在 logLevel > 0 时打印 DEBUG 级别
|
||||
if that.Log != nil && that.Log.GetLevel() > 0 {
|
||||
msg := fmt.Sprintf("SQL: %s DATA: %v", query, args)
|
||||
that.testLogBufMu.Lock()
|
||||
if that.testLogBuf != nil {
|
||||
that.testLogBuf.WriteString(msg + "\n")
|
||||
} else {
|
||||
that.Log.Debug().Msg(msg)
|
||||
}
|
||||
that.testLogBufMu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// processArgs 处理参数中的 slice 类型
|
||||
@@ -587,7 +575,9 @@ func (that *HoTimeDB) Row(resl *sql.Rows) []Map {
|
||||
b[j] = &a[j]
|
||||
}
|
||||
if err := resl.Scan(b...); err != nil {
|
||||
that.LastErr.SetError(err)
|
||||
dbErr := &DBError{}
|
||||
dbErr.SetError(err)
|
||||
that.lastError.Store(dbErr)
|
||||
return nil
|
||||
}
|
||||
for j := 0; j < colCount; j++ {
|
||||
@@ -615,7 +605,9 @@ func (that *HoTimeDB) Row(resl *sql.Rows) []Map {
|
||||
dest = append(dest, lis)
|
||||
}
|
||||
if err := resl.Err(); err != nil {
|
||||
that.LastErr.SetError(err)
|
||||
dbErr := &DBError{}
|
||||
dbErr.SetError(err)
|
||||
that.lastError.Store(dbErr)
|
||||
}
|
||||
return dest
|
||||
}
|
||||
|
||||
+9
-9
@@ -23,16 +23,11 @@ func (that *HoTimeDB) Action(action func(db HoTimeDB) (isSuccess bool)) (isSucce
|
||||
Log: that.Log,
|
||||
Type: that.Type,
|
||||
Prefix: that.Prefix,
|
||||
LastQuery: that.LastQuery,
|
||||
LastData: that.LastData,
|
||||
ConnectFunc: that.ConnectFunc,
|
||||
LastErr: that.LastErr,
|
||||
limit: that.limit,
|
||||
Tx: that.Tx,
|
||||
SlaveDB: that.SlaveDB,
|
||||
Mode: that.Mode,
|
||||
Dialect: that.Dialect,
|
||||
mu: sync.RWMutex{},
|
||||
limitMu: sync.Mutex{},
|
||||
testTx: that.testTx,
|
||||
testMu: that.testMu,
|
||||
@@ -76,7 +71,9 @@ func (that *HoTimeDB) Action(action func(db HoTimeDB) (isSuccess bool)) (isSucce
|
||||
tx, err := db.Begin()
|
||||
|
||||
if err != nil {
|
||||
that.LastErr.SetError(err)
|
||||
dbErr := &DBError{}
|
||||
dbErr.SetError(err)
|
||||
that.lastError.Store(dbErr)
|
||||
return isSuccess
|
||||
}
|
||||
|
||||
@@ -84,7 +81,6 @@ func (that *HoTimeDB) Action(action func(db HoTimeDB) (isSuccess bool)) (isSucce
|
||||
|
||||
isSuccess = action(db)
|
||||
|
||||
// SQL 出过错 → 事务必须回滚,不管回调返回什么
|
||||
if txFailed {
|
||||
_ = db.Tx.Rollback()
|
||||
return false
|
||||
@@ -93,7 +89,9 @@ func (that *HoTimeDB) Action(action func(db HoTimeDB) (isSuccess bool)) (isSucce
|
||||
if !isSuccess {
|
||||
err = db.Tx.Rollback()
|
||||
if err != nil {
|
||||
that.LastErr.SetError(err)
|
||||
dbErr := &DBError{}
|
||||
dbErr.SetError(err)
|
||||
that.lastError.Store(dbErr)
|
||||
return isSuccess
|
||||
}
|
||||
return isSuccess
|
||||
@@ -101,7 +99,9 @@ func (that *HoTimeDB) Action(action func(db HoTimeDB) (isSuccess bool)) (isSucce
|
||||
|
||||
err = db.Tx.Commit()
|
||||
if err != nil {
|
||||
that.LastErr.SetError(err)
|
||||
dbErr := &DBError{}
|
||||
dbErr.SetError(err)
|
||||
that.lastError.Store(dbErr)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
|
||||
@@ -2,11 +2,10 @@ package db
|
||||
|
||||
import (
|
||||
. "code.hoteas.com/golang/hotime/common"
|
||||
"code.hoteas.com/golang/hotime/log"
|
||||
"database/sql"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func newTestDB(t *testing.T) *HoTimeDB {
|
||||
@@ -15,14 +14,11 @@ func newTestDB(t *testing.T) *HoTimeDB {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
logger := logrus.New()
|
||||
logger.SetLevel(logrus.WarnLevel)
|
||||
db := &HoTimeDB{
|
||||
DB: sqlDB,
|
||||
Type: "sqlite3",
|
||||
Dialect: &SQLiteDialect{},
|
||||
LastErr: &Error{Logger: logger},
|
||||
Log: logger,
|
||||
Log: log.NewTestLogger(),
|
||||
}
|
||||
_, execErr := sqlDB.Exec("CREATE TABLE test_item (id INTEGER PRIMARY KEY, name TEXT, value INTEGER)")
|
||||
if execErr != nil {
|
||||
|
||||
+2
-2
@@ -55,13 +55,13 @@ func testDmRawSQL(that *Context) Map {
|
||||
testArticle := that.Db.Get("article", "id", Map{"state": 0})
|
||||
if testArticle != nil {
|
||||
res, err := that.Db.Exec(`UPDATE "article" SET "modify_time" = NOW() WHERE "id" = ?`, testArticle.GetInt64("id"))
|
||||
if err.GetError() == nil && res != nil {
|
||||
if err == nil && res != nil {
|
||||
affected, _ := res.RowsAffected()
|
||||
test2["result"] = affected >= 0
|
||||
test2["affected"] = affected
|
||||
} else {
|
||||
test2["result"] = false
|
||||
test2["error"] = err.GetError()
|
||||
test2["error"] = err
|
||||
}
|
||||
} else {
|
||||
test2["result"] = true
|
||||
|
||||
@@ -55,13 +55,13 @@ func testMysqlRawSQL(that *Context) Map {
|
||||
testArticle := that.Db.Get("article", "id", Map{"state": 0})
|
||||
if testArticle != nil {
|
||||
res, err := that.Db.Exec("UPDATE `article` SET modify_time = NOW() WHERE id = ?", testArticle.GetInt64("id"))
|
||||
if err.GetError() == nil && res != nil {
|
||||
if err == nil && res != nil {
|
||||
affected, _ := res.RowsAffected()
|
||||
test2["result"] = affected >= 0
|
||||
test2["affected"] = affected
|
||||
} else {
|
||||
test2["result"] = false
|
||||
test2["error"] = err.GetError()
|
||||
test2["error"] = err
|
||||
}
|
||||
} else {
|
||||
test2["result"] = true
|
||||
@@ -73,21 +73,21 @@ func testMysqlRawSQL(that *Context) Map {
|
||||
articles3 := that.Db.Query("SELECT id, title FROM `article` WHERE id IN (?) LIMIT 10", []int{1, 2, 3, 4, 5})
|
||||
test3["result"] = len(articles3) >= 0
|
||||
test3["count"] = len(articles3)
|
||||
test3["lastQuery"] = that.Db.LastQuery
|
||||
test3["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test3)
|
||||
|
||||
test4 := Map{"name": "IN (?) 空数组替换为1=0"}
|
||||
articles4 := that.Db.Query("SELECT id, title FROM `article` WHERE id IN (?) LIMIT 10", []int{})
|
||||
test4["result"] = len(articles4) == 0
|
||||
test4["count"] = len(articles4)
|
||||
test4["lastQuery"] = that.Db.LastQuery
|
||||
test4["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test4)
|
||||
|
||||
test5 := Map{"name": "NOT IN (?) 空数组替换为1=1"}
|
||||
articles5 := that.Db.Query("SELECT id, title FROM `article` WHERE id NOT IN (?) LIMIT 10", []int{})
|
||||
test5["result"] = len(articles5) > 0
|
||||
test5["count"] = len(articles5)
|
||||
test5["lastQuery"] = that.Db.LastQuery
|
||||
test5["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test5)
|
||||
|
||||
result["tests"] = tests
|
||||
|
||||
+19
-19
@@ -157,7 +157,7 @@ func testBasicCRUD(that *Context) Map {
|
||||
})
|
||||
insertTest["result"] = adminId > 0
|
||||
insertTest["adminId"] = adminId
|
||||
insertTest["lastQuery"] = that.Db.LastQuery
|
||||
insertTest["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, insertTest)
|
||||
|
||||
getTest := Map{"name": "Get 获取单条记录测试"}
|
||||
@@ -181,7 +181,7 @@ func testBasicCRUD(that *Context) Map {
|
||||
})
|
||||
selectTest2["result"] = true
|
||||
selectTest2["count"] = len(admins2)
|
||||
selectTest2["lastQuery"] = that.Db.LastQuery
|
||||
selectTest2["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, selectTest2)
|
||||
|
||||
updateTest := Map{"name": "Update 更新测试"}
|
||||
@@ -224,7 +224,7 @@ func testConditionSyntax(that *Context) Map {
|
||||
articles2 := that.Db.Select("article", "id,title,state", Map{"state[!]": -1, "LIMIT": 3})
|
||||
test2["result"] = true
|
||||
test2["count"] = len(articles2)
|
||||
test2["lastQuery"] = that.Db.LastQuery
|
||||
test2["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test2)
|
||||
|
||||
test3 := Map{"name": "大于小于条件 ([>], [<])"}
|
||||
@@ -251,7 +251,7 @@ func testConditionSyntax(that *Context) Map {
|
||||
articles5 := that.Db.Select("article", "id,title", Map{"title[~]": "新闻", "LIMIT": 3})
|
||||
test5["result"] = true
|
||||
test5["count"] = len(articles5)
|
||||
test5["lastQuery"] = that.Db.LastQuery
|
||||
test5["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test5)
|
||||
|
||||
test6 := Map{"name": "右模糊查询 ([~!])"}
|
||||
@@ -264,7 +264,7 @@ func testConditionSyntax(that *Context) Map {
|
||||
articles7 := that.Db.Select("article", "id,title,click_num", Map{"click_num[<>]": Slice{0, 1000}, "LIMIT": 3})
|
||||
test7["result"] = true
|
||||
test7["count"] = len(articles7)
|
||||
test7["lastQuery"] = that.Db.LastQuery
|
||||
test7["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test7)
|
||||
|
||||
test8 := Map{"name": "NOT BETWEEN 查询 ([><])"}
|
||||
@@ -277,7 +277,7 @@ func testConditionSyntax(that *Context) Map {
|
||||
articles9 := that.Db.Select("article", "id,title", Map{"id": Slice{1, 2, 3, 4, 5}, "LIMIT": 5})
|
||||
test9["result"] = true
|
||||
test9["count"] = len(articles9)
|
||||
test9["lastQuery"] = that.Db.LastQuery
|
||||
test9["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test9)
|
||||
|
||||
test10 := Map{"name": "NOT IN 查询 ([!])"}
|
||||
@@ -313,7 +313,7 @@ func testConditionSyntax(that *Context) Map {
|
||||
})
|
||||
test13["result"] = true
|
||||
test13["count"] = len(articles13)
|
||||
test13["lastQuery"] = that.Db.LastQuery
|
||||
test13["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test13)
|
||||
|
||||
test14 := Map{"name": "显式 AND 条件"}
|
||||
@@ -353,7 +353,7 @@ func testConditionSyntax(that *Context) Map {
|
||||
})
|
||||
test16["result"] = true
|
||||
test16["count"] = len(articles16)
|
||||
test16["lastQuery"] = that.Db.LastQuery
|
||||
test16["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test16)
|
||||
|
||||
result["tests"] = tests
|
||||
@@ -494,7 +494,7 @@ func testJoinQuery(that *Context) Map {
|
||||
})
|
||||
test2["result"] = len(articles2) >= 0
|
||||
test2["count"] = len(articles2)
|
||||
test2["lastQuery"] = that.Db.LastQuery
|
||||
test2["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test2)
|
||||
|
||||
test3 := Map{"name": "多表 JOIN"}
|
||||
@@ -544,28 +544,28 @@ func testAggregate(that *Context) Map {
|
||||
sum3 := that.Db.Sum("article", "click_num", Map{"state": 0})
|
||||
test3["result"] = sum3 >= 0
|
||||
test3["sum"] = sum3
|
||||
test3["lastQuery"] = that.Db.LastQuery
|
||||
test3["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test3)
|
||||
|
||||
test4 := Map{"name": "Avg 平均值 (单字段名)"}
|
||||
avg4 := that.Db.Avg("article", "click_num", Map{"state": 0})
|
||||
test4["result"] = avg4 >= 0
|
||||
test4["avg"] = avg4
|
||||
test4["lastQuery"] = that.Db.LastQuery
|
||||
test4["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test4)
|
||||
|
||||
test5 := Map{"name": "Max 最大值 (单字段名)"}
|
||||
max5 := that.Db.Max("article", "click_num", Map{"state": 0})
|
||||
test5["result"] = max5 >= 0
|
||||
test5["max"] = max5
|
||||
test5["lastQuery"] = that.Db.LastQuery
|
||||
test5["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test5)
|
||||
|
||||
test6 := Map{"name": "Min 最小值 (单字段名)"}
|
||||
min6 := that.Db.Min("article", "sort", Map{"state": 0})
|
||||
test6["result"] = true
|
||||
test6["min"] = min6
|
||||
test6["lastQuery"] = that.Db.LastQuery
|
||||
test6["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test6)
|
||||
|
||||
test7 := Map{"name": "GROUP BY 分组统计"}
|
||||
@@ -586,7 +586,7 @@ func testAggregate(that *Context) Map {
|
||||
test8["result"] = sum8 >= 0
|
||||
test8["sum"] = sum8
|
||||
test8["match_single_field"] = sum8 == sum3
|
||||
test8["lastQuery"] = that.Db.LastQuery
|
||||
test8["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test8)
|
||||
|
||||
test9 := Map{"name": "聚合函数一致性验证"}
|
||||
@@ -606,14 +606,14 @@ func testAggregate(that *Context) Map {
|
||||
sum10 := that.Db.Sum("article", "article.click_num", joinSlice, Map{"article.state": 0})
|
||||
test10["result"] = sum10 >= 0
|
||||
test10["sum"] = sum10
|
||||
test10["lastQuery"] = that.Db.LastQuery
|
||||
test10["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test10)
|
||||
|
||||
test11 := Map{"name": "Count 带 JOIN"}
|
||||
count11 := that.Db.Count("article", joinSlice, Map{"article.state": 0})
|
||||
test11["result"] = count11 >= 0
|
||||
test11["count"] = count11
|
||||
test11["lastQuery"] = that.Db.LastQuery
|
||||
test11["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test11)
|
||||
|
||||
result["tests"] = tests
|
||||
@@ -661,7 +661,7 @@ func testPagination(that *Context) Map {
|
||||
Select("id,title")
|
||||
test4["result"] = len(articles4) <= 3
|
||||
test4["count"] = len(articles4)
|
||||
test4["lastQuery"] = that.Db.LastQuery
|
||||
test4["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test4)
|
||||
|
||||
result["tests"] = tests
|
||||
@@ -683,7 +683,7 @@ func testInserts(that *Context) Map {
|
||||
})
|
||||
test1["result"] = affected1 >= 0
|
||||
test1["affected"] = affected1
|
||||
test1["lastQuery"] = that.Db.LastQuery
|
||||
test1["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test1)
|
||||
|
||||
test2 := Map{"name": "Inserts 带 [#] 标记"}
|
||||
@@ -729,7 +729,7 @@ func testUpsert(that *Context) Map {
|
||||
)
|
||||
test1["result"] = affected1 >= 0
|
||||
test1["affected"] = affected1
|
||||
test1["lastQuery"] = that.Db.LastQuery
|
||||
test1["lastQuery"] = that.Db.GetLastQuery()
|
||||
tests = append(tests, test1)
|
||||
|
||||
test2 := Map{"name": "Upsert 更新已存在记录"}
|
||||
|
||||
@@ -43,7 +43,6 @@ func TestBatchCacheOperations(app *hotime.Application) {
|
||||
// testCacheMemoryBatch 测试内存缓存批量操作
|
||||
func testCacheMemoryBatch(app *hotime.Application) {
|
||||
memCache := &cache.CacheMemory{TimeOut: 3600, DbSet: true, SessionSet: true}
|
||||
memCache.SetError(&Error{})
|
||||
|
||||
// 测试 CachesSet
|
||||
testData := Map{
|
||||
@@ -84,7 +83,6 @@ func testCacheDbBatch(app *hotime.Application) {
|
||||
Mode: cache.CacheModeNew,
|
||||
Db: &app.Db,
|
||||
}
|
||||
dbCache.SetError(&Error{})
|
||||
|
||||
// 清理测试数据
|
||||
dbCache.CachesDelete([]string{"db_batch_key1", "db_batch_key2", "db_batch_key3"})
|
||||
@@ -241,7 +239,6 @@ func testCacheBackfill(app *hotime.Application) {
|
||||
Mode: cache.CacheModeNew,
|
||||
Db: &app.Db,
|
||||
}
|
||||
dbCache.SetError(&Error{})
|
||||
|
||||
testKey := "backfill_test_key_" + ObjToStr(time.Now().UnixNano())
|
||||
testValue := Map{"backfill": "test_data"}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
module code.hoteas.com/golang/hotime
|
||||
|
||||
go 1.16
|
||||
go 1.23
|
||||
|
||||
require (
|
||||
gitee.com/chunanyong/dm v1.8.22
|
||||
@@ -9,10 +9,33 @@ require (
|
||||
github.com/go-pay/gopay v1.5.78
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/mattn/go-sqlite3 v1.14.12
|
||||
github.com/rs/zerolog v1.35.0
|
||||
github.com/silenceper/wechat/v2 v2.1.2
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.364
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ocr v1.0.364
|
||||
go.mongodb.org/mongo-driver v1.10.1
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect
|
||||
github.com/fatih/structs v1.1.0 // indirect
|
||||
github.com/golang/snappy v0.0.1 // indirect
|
||||
github.com/gomodule/redigo v1.8.5 // indirect
|
||||
github.com/klauspost/compress v1.13.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/spf13/cast v1.3.1 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.1.1 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.3 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d // indirect
|
||||
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d // indirect
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
)
|
||||
|
||||
@@ -30,6 +30,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN
|
||||
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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.12 h1:TJ1bhYJPV44phC+IMu1u2K/i5RriLTPe+yc68XDJ1Z0=
|
||||
github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
@@ -42,6 +46,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
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/rs/zerolog v1.35.0 h1:VD0ykx7HMiMJytqINBsKcbLS+BJ4WYjz+05us+LRTdI=
|
||||
github.com/rs/zerolog v1.35.0/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/silenceper/wechat/v2 v2.1.2 h1:+QfIMiYfwST2ZloTwmYp0O0p5Y1LYRZxfLWfMuSE30k=
|
||||
github.com/silenceper/wechat/v2 v2.1.2/go.mod h1:0OprxYCCp2CZAKw06BBlnaczInTk2KxOLsKeiopshGg=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
@@ -88,11 +94,11 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
||||
+429
@@ -0,0 +1,429 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// ErrorRecord 错误历史记录条目
|
||||
type ErrorRecord struct {
|
||||
Err error
|
||||
Msg string
|
||||
Time time.Time
|
||||
Caller string
|
||||
}
|
||||
|
||||
// Logger 日志核心结构体,封装 zerolog
|
||||
type Logger struct {
|
||||
zl zerolog.Logger
|
||||
logLevel int
|
||||
errors []ErrorRecord
|
||||
errIdx int // 环形缓冲写入位置
|
||||
errCount int // 实际写入总数(用于判断缓冲是否满)
|
||||
errorsMu sync.RWMutex
|
||||
maxErrors int
|
||||
}
|
||||
|
||||
// NewLogger 创建日志实例
|
||||
// logLevel: 0=仅 error,>=1=全部
|
||||
// logFile: 文件路径模板(如 "logs/20060102.txt"),空则不写文件
|
||||
// maxErrors: 错误历史最大条数,0 则不记录
|
||||
func NewLogger(logLevel int, logFile string, maxErrors int) *Logger {
|
||||
zerolog.CallerMarshalFunc = callerMarshalFunc
|
||||
zerolog.TimeFieldFormat = "2006-01-02 15:04:05"
|
||||
|
||||
var level zerolog.Level
|
||||
if logLevel == 0 {
|
||||
level = zerolog.ErrorLevel
|
||||
} else {
|
||||
level = zerolog.DebugLevel
|
||||
}
|
||||
|
||||
consoleWriter := zerolog.ConsoleWriter{
|
||||
Out: os.Stderr,
|
||||
TimeFormat: "2006-01-02 15:04:05",
|
||||
NoColor: false,
|
||||
FormatLevel: formatLevelColor,
|
||||
FormatCaller: func(i interface{}) string {
|
||||
if i == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("[%s]", i)
|
||||
},
|
||||
}
|
||||
|
||||
var writers []io.Writer
|
||||
writers = append(writers, consoleWriter)
|
||||
|
||||
if logFile != "" {
|
||||
fw := &TemplateFileWriter{pathTemplate: logFile}
|
||||
writers = append(writers, fw)
|
||||
}
|
||||
|
||||
multi := zerolog.MultiLevelWriter(writers...)
|
||||
zl := zerolog.New(multi).
|
||||
Level(level).
|
||||
With().
|
||||
Timestamp().
|
||||
Caller().
|
||||
Logger()
|
||||
|
||||
l := &Logger{
|
||||
zl: zl,
|
||||
logLevel: logLevel,
|
||||
maxErrors: maxErrors,
|
||||
}
|
||||
if maxErrors > 0 {
|
||||
l.errors = make([]ErrorRecord, maxErrors)
|
||||
}
|
||||
|
||||
return l
|
||||
}
|
||||
|
||||
// NewTestLogger 创建用于测试的静默 Logger(输出到 io.Discard)
|
||||
func NewTestLogger() *Logger {
|
||||
zl := zerolog.New(io.Discard).Level(zerolog.Disabled)
|
||||
return &Logger{zl: zl, logLevel: 0}
|
||||
}
|
||||
|
||||
// SetOutput 设置日志输出(用于测试等场景)
|
||||
func (l *Logger) SetOutput(w io.Writer) {
|
||||
if w == io.Discard {
|
||||
l.zl = zerolog.New(w).Level(zerolog.Disabled)
|
||||
return
|
||||
}
|
||||
consoleWriter := zerolog.ConsoleWriter{
|
||||
Out: w,
|
||||
TimeFormat: "2006-01-02 15:04:05",
|
||||
NoColor: true,
|
||||
FormatLevel: formatLevelPlain,
|
||||
FormatCaller: func(i interface{}) string {
|
||||
if i == nil {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("[%s]", i)
|
||||
},
|
||||
}
|
||||
l.zl = zerolog.New(consoleWriter).
|
||||
With().
|
||||
Timestamp().
|
||||
Caller().
|
||||
Logger()
|
||||
}
|
||||
|
||||
// GetLevel 获取当前日志等级
|
||||
func (l *Logger) GetLevel() int {
|
||||
return l.logLevel
|
||||
}
|
||||
|
||||
// --- 链式调用 API ---
|
||||
// 以下方法兼容两种调用风格:
|
||||
// 无参数:返回 *zerolog.Event 用于链式调用,如 l.Error().Str("k","v").Msg("...")
|
||||
// 有参数:直接拼接并打印日志(logrus 兼容),返回 no-op Event
|
||||
|
||||
func (l *Logger) Debug(args ...interface{}) *zerolog.Event {
|
||||
if len(args) > 0 {
|
||||
l.zl.Debug().Msg(fmt.Sprint(args...))
|
||||
nop := zerolog.Nop()
|
||||
return nop.Debug()
|
||||
}
|
||||
return l.zl.Debug()
|
||||
}
|
||||
|
||||
func (l *Logger) Info(args ...interface{}) *zerolog.Event {
|
||||
if len(args) > 0 {
|
||||
l.zl.Info().Msg(fmt.Sprint(args...))
|
||||
nop := zerolog.Nop()
|
||||
return nop.Info()
|
||||
}
|
||||
return l.zl.Info()
|
||||
}
|
||||
|
||||
func (l *Logger) Warn(args ...interface{}) *zerolog.Event {
|
||||
if len(args) > 0 {
|
||||
l.zl.Warn().Msg(fmt.Sprint(args...))
|
||||
nop := zerolog.Nop()
|
||||
return nop.Warn()
|
||||
}
|
||||
return l.zl.Warn()
|
||||
}
|
||||
|
||||
func (l *Logger) Error(args ...interface{}) *zerolog.Event {
|
||||
if len(args) > 0 {
|
||||
msg := fmt.Sprint(args...)
|
||||
if l.maxErrors > 0 {
|
||||
l.recordError(msg, nil)
|
||||
}
|
||||
l.zl.Error().Msg(msg)
|
||||
nop := zerolog.Nop()
|
||||
return nop.Error()
|
||||
}
|
||||
if l.maxErrors > 0 {
|
||||
l.recordError("", nil)
|
||||
}
|
||||
return l.zl.Error()
|
||||
}
|
||||
|
||||
// RecordError 手动记录一条错误到历史(用于需要指定详情的场景)
|
||||
func (l *Logger) RecordError(err error, msg string, caller string) {
|
||||
if l.maxErrors <= 0 {
|
||||
return
|
||||
}
|
||||
l.errorsMu.Lock()
|
||||
defer l.errorsMu.Unlock()
|
||||
l.errors[l.errIdx] = ErrorRecord{
|
||||
Err: err,
|
||||
Msg: msg,
|
||||
Time: time.Now(),
|
||||
Caller: caller,
|
||||
}
|
||||
l.errIdx = (l.errIdx + 1) % l.maxErrors
|
||||
l.errCount++
|
||||
}
|
||||
|
||||
func (l *Logger) recordError(msg string, err error) {
|
||||
_, file, line, ok := runtime.Caller(2)
|
||||
caller := ""
|
||||
if ok {
|
||||
caller = formatCaller(file, line)
|
||||
}
|
||||
l.RecordError(err, msg, caller)
|
||||
}
|
||||
|
||||
// GetRecentErrors 获取最近 N 条错误(不传则返回全部已存储的)
|
||||
func (l *Logger) GetRecentErrors(n ...int) []ErrorRecord {
|
||||
if l.maxErrors <= 0 {
|
||||
return nil
|
||||
}
|
||||
l.errorsMu.RLock()
|
||||
defer l.errorsMu.RUnlock()
|
||||
|
||||
total := l.errCount
|
||||
if total > l.maxErrors {
|
||||
total = l.maxErrors
|
||||
}
|
||||
if total == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
want := total
|
||||
if len(n) > 0 && n[0] > 0 && n[0] < want {
|
||||
want = n[0]
|
||||
}
|
||||
|
||||
result := make([]ErrorRecord, 0, want)
|
||||
// 从最新的往前读
|
||||
for i := 0; i < want; i++ {
|
||||
idx := (l.errIdx - 1 - i + l.maxErrors) % l.maxErrors
|
||||
if l.errors[idx].Time.IsZero() {
|
||||
break
|
||||
}
|
||||
result = append(result, l.errors[idx])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// --- 格式化快捷方法 ---
|
||||
|
||||
func (l *Logger) Debugf(format string, v ...interface{}) {
|
||||
l.zl.Debug().Msgf(format, v...)
|
||||
}
|
||||
func (l *Logger) Infof(format string, v ...interface{}) {
|
||||
l.zl.Info().Msgf(format, v...)
|
||||
}
|
||||
func (l *Logger) Warnf(format string, v ...interface{}) {
|
||||
l.zl.Warn().Msgf(format, v...)
|
||||
}
|
||||
func (l *Logger) Errorf(format string, v ...interface{}) {
|
||||
if l.maxErrors > 0 {
|
||||
l.recordError(fmt.Sprintf(format, v...), nil)
|
||||
}
|
||||
l.zl.Error().Msgf(format, v...)
|
||||
}
|
||||
|
||||
// --- 日志级别颜色 ---
|
||||
|
||||
func formatLevelColor(i interface{}) string {
|
||||
level := strings.ToUpper(fmt.Sprintf("%s", i))
|
||||
switch level {
|
||||
case "DEBUG":
|
||||
return fmt.Sprintf("\x1b[36m|%s|\x1b[0m", level) // cyan
|
||||
case "INFO":
|
||||
return fmt.Sprintf("\x1b[32m|%s|\x1b[0m", level) // green
|
||||
case "WARN":
|
||||
return fmt.Sprintf("\x1b[33m|%s|\x1b[0m", level) // yellow
|
||||
case "ERROR":
|
||||
return fmt.Sprintf("\x1b[31m|%s|\x1b[0m", level) // red
|
||||
case "FATAL":
|
||||
return fmt.Sprintf("\x1b[35m|%s|\x1b[0m", level) // magenta
|
||||
default:
|
||||
return fmt.Sprintf("|%s|", level)
|
||||
}
|
||||
}
|
||||
|
||||
func formatLevelPlain(i interface{}) string {
|
||||
return fmt.Sprintf("|%s|", strings.ToUpper(fmt.Sprintf("%s", i)))
|
||||
}
|
||||
|
||||
// --- TemplateFileWriter ---
|
||||
|
||||
// TemplateFileWriter 按时间模板切换文件路径的 Writer
|
||||
type TemplateFileWriter struct {
|
||||
pathTemplate string
|
||||
mu sync.Mutex
|
||||
currentPath string
|
||||
file *os.File
|
||||
writer *bufio.Writer
|
||||
}
|
||||
|
||||
func (w *TemplateFileWriter) Write(p []byte) (n int, err error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
path := time.Now().Format(w.pathTemplate)
|
||||
if path != w.currentPath {
|
||||
if w.writer != nil {
|
||||
_ = w.writer.Flush()
|
||||
}
|
||||
if w.file != nil {
|
||||
_ = w.file.Close()
|
||||
}
|
||||
_ = os.MkdirAll(filepath.Dir(path), 0755)
|
||||
w.file, err = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
w.writer = bufio.NewWriterSize(w.file, 4096)
|
||||
w.currentPath = path
|
||||
}
|
||||
n, err = w.writer.Write(p)
|
||||
_ = w.writer.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
// --- 调用者智能过滤 ---
|
||||
|
||||
const maxFrameworkDepth = 10
|
||||
|
||||
func callerMarshalFunc(_ uintptr, file string, line int) string {
|
||||
return findCaller(file, line)
|
||||
}
|
||||
|
||||
func findCaller(origFile string, origLine int) string {
|
||||
frameworkCount := 0
|
||||
var lastFrameworkFile string
|
||||
var lastFrameworkLine int
|
||||
var applicationFile string
|
||||
var applicationLine int
|
||||
|
||||
for i := 1; i < 20; i++ {
|
||||
_, file, line, ok := runtime.Caller(i)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
shortFile := shortenPath(file)
|
||||
|
||||
if isHoTimeFrameworkFile(shortFile) {
|
||||
frameworkCount++
|
||||
lastFrameworkFile = shortFile
|
||||
lastFrameworkLine = line
|
||||
if strings.Contains(shortFile, "application.go") {
|
||||
applicationFile = shortFile
|
||||
applicationLine = line
|
||||
}
|
||||
if frameworkCount >= maxFrameworkDepth {
|
||||
return fmt.Sprintf("%s:%d", shortFile, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%d", shortFile, line)
|
||||
}
|
||||
|
||||
if applicationFile != "" {
|
||||
return fmt.Sprintf("%s:%d", applicationFile, applicationLine)
|
||||
}
|
||||
if lastFrameworkFile != "" {
|
||||
return fmt.Sprintf("%s:%d", lastFrameworkFile, lastFrameworkLine)
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", shortenPath(origFile), origLine)
|
||||
}
|
||||
|
||||
func formatCaller(file string, line int) string {
|
||||
return fmt.Sprintf("%s:%d", shortenPath(file), line)
|
||||
}
|
||||
|
||||
func shortenPath(file string) string {
|
||||
n := 0
|
||||
for i := len(file) - 1; i > 0; i-- {
|
||||
if file[i] == '/' || file[i] == '\\' {
|
||||
n++
|
||||
if n >= 2 {
|
||||
return file[i+1:]
|
||||
}
|
||||
}
|
||||
}
|
||||
return file
|
||||
}
|
||||
|
||||
func isHoTimeFrameworkFile(file string) bool {
|
||||
// zerolog 内部文件
|
||||
if strings.HasPrefix(file, "zerolog/") || strings.HasPrefix(file, "zerolog@") {
|
||||
return true
|
||||
}
|
||||
// logrus 遗留(vendor 中可能存在)
|
||||
if strings.HasPrefix(file, "logrus/") || strings.HasPrefix(file, "logrus@") {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(file, "runtime/") {
|
||||
return true
|
||||
}
|
||||
|
||||
lowerFile := strings.ToLower(file)
|
||||
if strings.Contains(lowerFile, "hotime") {
|
||||
frameworkDirs := []string{"db/", "common/", "code/", "cache/", "log/", "dri/"}
|
||||
for _, dir := range frameworkDirs {
|
||||
if strings.Contains(file, dir) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if strings.HasSuffix(file, "application.go") ||
|
||||
strings.HasSuffix(file, "context.go") ||
|
||||
strings.HasSuffix(file, "session.go") ||
|
||||
strings.HasSuffix(file, "const.go") ||
|
||||
strings.HasSuffix(file, "type.go") ||
|
||||
strings.HasSuffix(file, "var.go") ||
|
||||
strings.HasSuffix(file, "mime.go") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
frameworkCoreDirs := []string{"db/", "common/", "code/", "cache/"}
|
||||
for _, dir := range frameworkCoreDirs {
|
||||
if strings.HasPrefix(file, dir) {
|
||||
frameworkFiles := []string{
|
||||
"query.go", "crud.go", "where.go", "builder.go", "db.go",
|
||||
"dialect.go", "aggregate.go", "transaction.go", "identifier.go",
|
||||
"error.go", "func.go", "map.go", "obj.go", "slice.go",
|
||||
"makecode.go", "template.go", "config.go",
|
||||
"cache.go", "cache_db.go", "cache_memory.go", "cache_redis.go",
|
||||
}
|
||||
for _, f := range frameworkFiles {
|
||||
if strings.HasSuffix(file, f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
-222
@@ -1,222 +0,0 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func GetLog(path string, showCodeLine bool) *log.Logger {
|
||||
|
||||
hook := MyHook{
|
||||
Field: "line",
|
||||
Skip: 5,
|
||||
Path: path, ShowCodeLine: showCodeLine,
|
||||
}
|
||||
|
||||
loger := log.New()
|
||||
loger.SetFormatter(&log.TextFormatter{
|
||||
ForceColors: true,
|
||||
FullTimestamp: true,
|
||||
TimestampFormat: "2006-01-02 15:04:05",
|
||||
DisableLevelTruncation: true,
|
||||
})
|
||||
loger.AddHook(&hook)
|
||||
return loger
|
||||
}
|
||||
|
||||
// MyHook ...
|
||||
type MyHook struct {
|
||||
Path string //存储日志的位置
|
||||
ShowCodeLine bool //输出代码文件名称和日志行
|
||||
Field string
|
||||
Skip int
|
||||
levels []log.Level
|
||||
}
|
||||
|
||||
// Levels 只定义 error 和 panic 等级的日志,其他日志等级不会触发 hook
|
||||
func (that *MyHook) Levels() []log.Level {
|
||||
return log.AllLevels
|
||||
}
|
||||
|
||||
// Fire 将异常日志写入到指定日志文件中
|
||||
func (that *MyHook) Fire(entry *log.Entry) error {
|
||||
if that.ShowCodeLine {
|
||||
entry.Data[that.Field] = findCaller(that.Skip)
|
||||
}
|
||||
//不需要存储到文件
|
||||
if that.Path == "" {
|
||||
return nil
|
||||
}
|
||||
//存储到文件
|
||||
logFilePath := time.Now().Format(that.Path)
|
||||
|
||||
err := os.MkdirAll(filepath.Dir(logFilePath), os.ModeAppend)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//os.Create(logFilePath)
|
||||
f, err := os.OpenFile(logFilePath, os.O_APPEND|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bte, _ := entry.Bytes()
|
||||
_, err = f.Write(bte)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = f.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 最大框架层数限制 - 超过这个层数后不再跳过,防止误过滤应用层
|
||||
const maxFrameworkDepth = 10
|
||||
|
||||
// isHoTimeFrameworkFile 判断是否是 HoTime 框架文件
|
||||
// 更精确的匹配:只有明确属于框架的文件才会被跳过
|
||||
func isHoTimeFrameworkFile(file string) bool {
|
||||
// 1. logrus 日志库内部文件(支持带版本号的路径,如 logrus@v1.8.1/entry.go)
|
||||
if strings.HasPrefix(file, "logrus/") || strings.HasPrefix(file, "logrus@") {
|
||||
return true
|
||||
}
|
||||
|
||||
// 2. Go 运行时文件
|
||||
if strings.HasPrefix(file, "runtime/") {
|
||||
return true
|
||||
}
|
||||
|
||||
// 3. HoTime 框架核心文件 - 通过包含 "hotime" 或框架特有文件名来识别
|
||||
// 检查路径中是否包含 hotime 框架标识
|
||||
lowerFile := strings.ToLower(file)
|
||||
if strings.Contains(lowerFile, "hotime") {
|
||||
// 是 hotime 框架的一部分,检查是否是核心模块
|
||||
frameworkDirs := []string{"db/", "common/", "code/", "cache/", "log/", "dri/"}
|
||||
for _, dir := range frameworkDirs {
|
||||
if strings.Contains(file, dir) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
// 框架核心文件(在 hotime 根目录下的 .go 文件)
|
||||
if strings.HasSuffix(file, "application.go") ||
|
||||
strings.HasSuffix(file, "context.go") ||
|
||||
strings.HasSuffix(file, "session.go") ||
|
||||
strings.HasSuffix(file, "const.go") ||
|
||||
strings.HasSuffix(file, "type.go") ||
|
||||
strings.HasSuffix(file, "var.go") ||
|
||||
strings.HasSuffix(file, "mime.go") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// 4. 直接匹配框架核心目录(用于没有完整路径的情况)
|
||||
// 只匹配 "db/xxx.go" 这种在框架核心目录下的文件
|
||||
frameworkCoreDirs := []string{"db/", "common/", "code/", "cache/"}
|
||||
for _, dir := range frameworkCoreDirs {
|
||||
if strings.HasPrefix(file, dir) {
|
||||
// 额外检查:确保不是用户项目中同名目录
|
||||
// 框架文件通常有特定的文件名
|
||||
frameworkFiles := []string{
|
||||
"query.go", "crud.go", "where.go", "builder.go", "db.go",
|
||||
"dialect.go", "aggregate.go", "transaction.go", "identifier.go",
|
||||
"error.go", "func.go", "map.go", "obj.go", "slice.go",
|
||||
"makecode.go", "template.go", "config.go",
|
||||
"cache.go", "cache_db.go", "cache_memory.go", "cache_redis.go",
|
||||
}
|
||||
for _, f := range frameworkFiles {
|
||||
if strings.HasSuffix(file, f) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// 对caller进行递归查询, 直到找到非框架层产生的第一个调用.
|
||||
// 遍历调用栈,跳过框架层文件,找到应用层代码
|
||||
// 使用层数限制确保不会误过滤应用层同名目录
|
||||
// 返回优先级:应用层代码 > application.go > 其他框架文件
|
||||
func findCaller(skip int) string {
|
||||
frameworkCount := 0 // 连续框架层计数
|
||||
var lastFrameworkFile string
|
||||
var lastFrameworkLine int
|
||||
var applicationFile string // 优先记录 application.go 位置
|
||||
var applicationLine int
|
||||
|
||||
// 遍历调用栈,找到第一个非框架文件
|
||||
for i := 0; i < 20; i++ {
|
||||
file, line := getCaller(skip + i)
|
||||
if file == "" {
|
||||
break
|
||||
}
|
||||
|
||||
isFramework := isHoTimeFrameworkFile(file)
|
||||
|
||||
if isFramework {
|
||||
frameworkCount++
|
||||
lastFrameworkFile = file
|
||||
lastFrameworkLine = line
|
||||
// 优先记录 application.go 位置(HoTime 框架入口)
|
||||
if strings.Contains(file, "application.go") {
|
||||
applicationFile = file
|
||||
applicationLine = line
|
||||
}
|
||||
// 层数限制:如果已经跳过太多层,停止跳过
|
||||
if frameworkCount >= maxFrameworkDepth {
|
||||
return fmt.Sprintf("%s:%d", file, line)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// 找到非框架文件,返回应用层代码位置
|
||||
return fmt.Sprintf("%s:%d", file, line)
|
||||
}
|
||||
|
||||
// 如果找不到应用层,返回最后记录的框架文件位置
|
||||
// 优先级:application.go > 其他框架文件 > 第一个调用者
|
||||
// 确保不会返回 logrus 或 runtime 等三方组件位置
|
||||
if applicationFile != "" {
|
||||
return fmt.Sprintf("%s:%d", applicationFile, applicationLine)
|
||||
}
|
||||
|
||||
if lastFrameworkFile != "" {
|
||||
return fmt.Sprintf("%s:%d", lastFrameworkFile, lastFrameworkLine)
|
||||
}
|
||||
|
||||
// 最后的回退:返回第一个调用者
|
||||
file, line := getCaller(skip)
|
||||
return fmt.Sprintf("%s:%d", file, line)
|
||||
}
|
||||
|
||||
// 这里其实可以获取函数名称的: fnName := runtime.FuncForPC(pc).Name()
|
||||
// 但是我觉得有 文件名和行号就够定位问题, 因此忽略了caller返回的第一个值:pc
|
||||
// 在标准库log里面我们可以选择记录文件的全路径或者文件名, 但是在使用过程成并发最合适的,
|
||||
// 因为文件的全路径往往很长, 而文件名在多个包中往往有重复, 因此这里选择多取一层, 取到文件所在的上层目录那层.
|
||||
func getCaller(skip int) (string, int) {
|
||||
_, file, line, ok := runtime.Caller(skip)
|
||||
if !ok {
|
||||
return "", 0
|
||||
}
|
||||
n := 0
|
||||
for i := len(file) - 1; i > 0; i-- {
|
||||
if file[i] == '/' || file[i] == '\\' { // 同时处理 / 和 \ 路径分隔符
|
||||
n++
|
||||
if n >= 2 {
|
||||
file = file[i+1:]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return file, line
|
||||
}
|
||||
+1
-2
@@ -27,8 +27,7 @@ func Run() {
|
||||
}
|
||||
|
||||
appIns.Config["tpt"] = TptPath
|
||||
appIns.SetSession(hotime.CacheIns(&hotime.CacheMemory{}), hotime.CacheIns(&hotime.CacheMemory{}))
|
||||
appIns.SetCache(hotime.CacheIns(&hotime.CacheMemory{}))
|
||||
appIns.SetCache()
|
||||
|
||||
//快捷模式
|
||||
//appIns.SetDefault(func(err ...*hotime.Error) *sql.DB {
|
||||
|
||||
@@ -107,8 +107,6 @@ func NewTestApp(configPath string, projects TestProj, listeners ...func(*Context
|
||||
devNull.Close()
|
||||
|
||||
app.Log.SetOutput(io.Discard)
|
||||
origMode := app.Db.Mode
|
||||
app.Db.Mode = 0
|
||||
|
||||
for _, lis := range listeners {
|
||||
app.SetConnectListener(lis)
|
||||
@@ -124,7 +122,6 @@ func NewTestApp(configPath string, projects TestProj, listeners ...func(*Context
|
||||
app.HoTimeCache.DisableDbCache()
|
||||
}
|
||||
|
||||
app.Db.Mode = origMode
|
||||
app.Log.SetOutput(os.Stderr)
|
||||
|
||||
return &TestApp{
|
||||
|
||||
@@ -10,7 +10,7 @@ var App = map[string]*Application{} //整个项目
|
||||
//var Db = HoTimeDB{} //数据库实例
|
||||
|
||||
var Config = Map{
|
||||
"mode": 2, //模式 0生产模式,1,测试模式,2,开发模式
|
||||
"logLevel": 1, // 0=仅错误,>=1=全部日志
|
||||
//"codeConfig": Map{
|
||||
// "admin": "config/app.json",
|
||||
//},
|
||||
@@ -56,11 +56,11 @@ var Config = Map{
|
||||
}
|
||||
|
||||
var ConfigNote = Map{
|
||||
"logLevel": "默认0,必须,0关闭,1打印,日志等级",
|
||||
"logLevel": "默认1,必须,0=仅打印错误日志,>=1=打印全部日志(debug/info/warn/error),同时控制SQL日志和Cache-Control(0=public,>=1=no-cache)",
|
||||
"logFile": "无默认,非必须,如果需要存储日志文件时使用,保存格式为:a/b/c/20060102150405.txt,将生成:a/b/c/年月日时分秒.txt,按需设置",
|
||||
"logHistory": "默认100,非必须,内存中保留最近N条错误日志,用于调试调阅,通过 Log.GetRecentErrors() 获取",
|
||||
"webConnectLogShow": "默认true,非必须,访问日志如果需要web访问链接、访问ip、访问时间打印,false为关闭true开启此功能",
|
||||
"webConnectLogFile": "无默认,非必须,webConnectLogShow开启之后才能使用,如果需要存储日志文件时使用,保存格式为:a/b/c/20060102150405.txt,将生成:a/b/c/年月日时分秒.txt,按需设置",
|
||||
"mode": "默认0,非必须,0生产模式,1,测试模式,2开发模式,3内嵌代码模式,在开发模式下会显示更多的数据用于开发测试,并能够辅助研发,自动生成配置文件、代码等功能,web无缓存,数据库不启用缓存", //debug 0关闭1开启
|
||||
//"codeConfig": Map{
|
||||
// "注释": "配置即启用,非必须,默认无",
|
||||
// //"package":"默认admin,必须,mode模式为2时会自动生成包文件夹和代码文件",
|
||||
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
module gitee.com/chunanyong/dm
|
||||
|
||||
go 1.13
|
||||
|
||||
require (
|
||||
github.com/golang/snappy v0.0.1
|
||||
golang.org/x/text v0.3.2
|
||||
)
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
module github.com/360EntSecGroup-Skylar/excelize
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb
|
||||
)
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
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/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb h1:cRItZejS4Ok67vfCdrbGIaqk86wmtQNOjVD7jSyS2aw=
|
||||
github.com/stretchr/testify v1.2.3-0.20181224173747-660f15d67dbb/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
module github.com/go-pay/gopay
|
||||
|
||||
go 1.16
|
||||
|
||||
require golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29 h1:tkVvjkPTB7pnW3jnid7kNyAMPVWllTNOf/qKDze4p9o=
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
module github.com/go-sql-driver/mysql
|
||||
|
||||
go 1.10
|
||||
-1
@@ -1 +0,0 @@
|
||||
module github.com/golang/snappy
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
module github.com/klauspost/compress
|
||||
|
||||
go 1.15
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2016 Yasuhiro Matsumoto
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
# go-colorable
|
||||
|
||||
[](https://github.com/mattn/go-colorable/actions?query=workflow%3Atest)
|
||||
[](https://codecov.io/gh/mattn/go-colorable)
|
||||
[](http://godoc.org/github.com/mattn/go-colorable)
|
||||
[](https://goreportcard.com/report/mattn/go-colorable)
|
||||
|
||||
Colorable writer for windows.
|
||||
|
||||
For example, most of logger packages doesn't show colors on windows. (I know we can do it with ansicon. But I don't want.)
|
||||
This package is possible to handle escape sequence for ansi color on windows.
|
||||
|
||||
## Too Bad!
|
||||
|
||||

|
||||
|
||||
|
||||
## So Good!
|
||||
|
||||

|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})
|
||||
logrus.SetOutput(colorable.NewColorableStdout())
|
||||
|
||||
logrus.Info("succeeded")
|
||||
logrus.Warn("not correct")
|
||||
logrus.Error("something error")
|
||||
logrus.Fatal("panic")
|
||||
```
|
||||
|
||||
You can compile above code on non-windows OSs.
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
$ go get github.com/mattn/go-colorable
|
||||
```
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
|
||||
# Author
|
||||
|
||||
Yasuhiro Matsumoto (a.k.a mattn)
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
//go:build !windows || appengine
|
||||
// +build !windows appengine
|
||||
|
||||
package colorable
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
_ "github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
// NewColorable returns new instance of Writer which handles escape sequence.
|
||||
func NewColorable(file *os.File) io.Writer {
|
||||
if file == nil {
|
||||
panic("nil passed instead of *os.File to NewColorable()")
|
||||
}
|
||||
|
||||
return file
|
||||
}
|
||||
|
||||
// NewColorableStdout returns new instance of Writer which handles escape sequence for stdout.
|
||||
func NewColorableStdout() io.Writer {
|
||||
return os.Stdout
|
||||
}
|
||||
|
||||
// NewColorableStderr returns new instance of Writer which handles escape sequence for stderr.
|
||||
func NewColorableStderr() io.Writer {
|
||||
return os.Stderr
|
||||
}
|
||||
|
||||
// EnableColorsStdout enable colors if possible.
|
||||
func EnableColorsStdout(enabled *bool) func() {
|
||||
if enabled != nil {
|
||||
*enabled = true
|
||||
}
|
||||
return func() {}
|
||||
}
|
||||
+1047
File diff suppressed because it is too large
Load Diff
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
echo "" > coverage.txt
|
||||
|
||||
for d in $(go list ./... | grep -v vendor); do
|
||||
go test -race -coverprofile=profile.out -covermode=atomic "$d"
|
||||
if [ -f profile.out ]; then
|
||||
cat profile.out >> coverage.txt
|
||||
rm profile.out
|
||||
fi
|
||||
done
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
package colorable
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
// NonColorable holds writer but removes escape sequence.
|
||||
type NonColorable struct {
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
// NewNonColorable returns new instance of Writer which removes escape sequence from Writer.
|
||||
func NewNonColorable(w io.Writer) io.Writer {
|
||||
return &NonColorable{out: w}
|
||||
}
|
||||
|
||||
// Write writes data on console
|
||||
func (w *NonColorable) Write(data []byte) (n int, err error) {
|
||||
er := bytes.NewReader(data)
|
||||
var plaintext bytes.Buffer
|
||||
loop:
|
||||
for {
|
||||
c1, err := er.ReadByte()
|
||||
if err != nil {
|
||||
plaintext.WriteTo(w.out)
|
||||
break loop
|
||||
}
|
||||
if c1 != 0x1b {
|
||||
plaintext.WriteByte(c1)
|
||||
continue
|
||||
}
|
||||
_, err = plaintext.WriteTo(w.out)
|
||||
if err != nil {
|
||||
break loop
|
||||
}
|
||||
c2, err := er.ReadByte()
|
||||
if err != nil {
|
||||
break loop
|
||||
}
|
||||
if c2 != 0x5b {
|
||||
continue
|
||||
}
|
||||
|
||||
for {
|
||||
c, err := er.ReadByte()
|
||||
if err != nil {
|
||||
break loop
|
||||
}
|
||||
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return len(data), nil
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
Copyright (c) Yasuhiro MATSUMOTO <mattn.jp@gmail.com>
|
||||
|
||||
MIT License (Expat)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
# go-isatty
|
||||
|
||||
[](http://godoc.org/github.com/mattn/go-isatty)
|
||||
[](https://codecov.io/gh/mattn/go-isatty)
|
||||
[](https://coveralls.io/github/mattn/go-isatty?branch=master)
|
||||
[](https://goreportcard.com/report/mattn/go-isatty)
|
||||
|
||||
isatty for golang
|
||||
|
||||
## Usage
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mattn/go-isatty"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||
fmt.Println("Is Terminal")
|
||||
} else if isatty.IsCygwinTerminal(os.Stdout.Fd()) {
|
||||
fmt.Println("Is Cygwin/MSYS2 Terminal")
|
||||
} else {
|
||||
fmt.Println("Is Not Terminal")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Installation
|
||||
|
||||
```
|
||||
$ go get github.com/mattn/go-isatty
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
## Author
|
||||
|
||||
Yasuhiro Matsumoto (a.k.a mattn)
|
||||
|
||||
## Thanks
|
||||
|
||||
* k-takata: base idea for IsCygwinTerminal
|
||||
|
||||
https://github.com/k-takata/go-iscygpty
|
||||
+2
@@ -0,0 +1,2 @@
|
||||
// Package isatty implements interface to isatty
|
||||
package isatty
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
echo "" > coverage.txt
|
||||
|
||||
for d in $(go list ./... | grep -v vendor); do
|
||||
go test -race -coverprofile=profile.out -covermode=atomic "$d"
|
||||
if [ -f profile.out ]; then
|
||||
cat profile.out >> coverage.txt
|
||||
rm profile.out
|
||||
fi
|
||||
done
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
//go:build (darwin || freebsd || openbsd || netbsd || dragonfly || hurd) && !appengine && !tinygo
|
||||
// +build darwin freebsd openbsd netbsd dragonfly hurd
|
||||
// +build !appengine
|
||||
// +build !tinygo
|
||||
|
||||
package isatty
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
// IsTerminal return true if the file descriptor is terminal.
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
_, err := unix.IoctlGetTermios(int(fd), unix.TIOCGETA)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
|
||||
// terminal. This is also always false on this environment.
|
||||
func IsCygwinTerminal(fd uintptr) bool {
|
||||
return false
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
//go:build (appengine || js || nacl || tinygo || wasm) && !windows
|
||||
// +build appengine js nacl tinygo wasm
|
||||
// +build !windows
|
||||
|
||||
package isatty
|
||||
|
||||
// IsTerminal returns true if the file descriptor is terminal which
|
||||
// is always false on js and appengine classic which is a sandboxed PaaS.
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2
|
||||
// terminal. This is also always false on this environment.
|
||||
func IsCygwinTerminal(fd uintptr) bool {
|
||||
return false
|
||||
}
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
//go:build plan9
|
||||
// +build plan9
|
||||
|
||||
package isatty
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
path, err := syscall.Fd2path(int(fd))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return path == "/dev/cons" || path == "/mnt/term/dev/cons"
|
||||
}
|
||||
|
||||
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
|
||||
// terminal. This is also always false on this environment.
|
||||
func IsCygwinTerminal(fd uintptr) bool {
|
||||
return false
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
//go:build solaris && !appengine
|
||||
// +build solaris,!appengine
|
||||
|
||||
package isatty
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||
// see: https://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libc/port/gen/isatty.c
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
_, err := unix.IoctlGetTermio(int(fd), unix.TCGETA)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
|
||||
// terminal. This is also always false on this environment.
|
||||
func IsCygwinTerminal(fd uintptr) bool {
|
||||
return false
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
//go:build (linux || aix || zos) && !appengine && !tinygo
|
||||
// +build linux aix zos
|
||||
// +build !appengine
|
||||
// +build !tinygo
|
||||
|
||||
package isatty
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
// IsTerminal return true if the file descriptor is terminal.
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
_, err := unix.IoctlGetTermios(int(fd), unix.TCGETS)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsCygwinTerminal return true if the file descriptor is a cygwin or msys2
|
||||
// terminal. This is also always false on this environment.
|
||||
func IsCygwinTerminal(fd uintptr) bool {
|
||||
return false
|
||||
}
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
//go:build windows && !appengine
|
||||
// +build windows,!appengine
|
||||
|
||||
package isatty
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"syscall"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
objectNameInfo uintptr = 1
|
||||
fileNameInfo = 2
|
||||
fileTypePipe = 3
|
||||
)
|
||||
|
||||
var (
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
ntdll = syscall.NewLazyDLL("ntdll.dll")
|
||||
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
|
||||
procGetFileInformationByHandleEx = kernel32.NewProc("GetFileInformationByHandleEx")
|
||||
procGetFileType = kernel32.NewProc("GetFileType")
|
||||
procNtQueryObject = ntdll.NewProc("NtQueryObject")
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Check if GetFileInformationByHandleEx is available.
|
||||
if procGetFileInformationByHandleEx.Find() != nil {
|
||||
procGetFileInformationByHandleEx = nil
|
||||
}
|
||||
}
|
||||
|
||||
// IsTerminal return true if the file descriptor is terminal.
|
||||
func IsTerminal(fd uintptr) bool {
|
||||
var st uint32
|
||||
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0)
|
||||
return r != 0 && e == 0
|
||||
}
|
||||
|
||||
// Check pipe name is used for cygwin/msys2 pty.
|
||||
// Cygwin/MSYS2 PTY has a name like:
|
||||
// \{cygwin,msys}-XXXXXXXXXXXXXXXX-ptyN-{from,to}-master
|
||||
func isCygwinPipeName(name string) bool {
|
||||
token := strings.Split(name, "-")
|
||||
if len(token) < 5 {
|
||||
return false
|
||||
}
|
||||
|
||||
if token[0] != `\msys` &&
|
||||
token[0] != `\cygwin` &&
|
||||
token[0] != `\Device\NamedPipe\msys` &&
|
||||
token[0] != `\Device\NamedPipe\cygwin` {
|
||||
return false
|
||||
}
|
||||
|
||||
if token[1] == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(token[2], "pty") {
|
||||
return false
|
||||
}
|
||||
|
||||
if token[3] != `from` && token[3] != `to` {
|
||||
return false
|
||||
}
|
||||
|
||||
if token[4] != "master" {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// getFileNameByHandle use the undocomented ntdll NtQueryObject to get file full name from file handler
|
||||
// since GetFileInformationByHandleEx is not available under windows Vista and still some old fashion
|
||||
// guys are using Windows XP, this is a workaround for those guys, it will also work on system from
|
||||
// Windows vista to 10
|
||||
// see https://stackoverflow.com/a/18792477 for details
|
||||
func getFileNameByHandle(fd uintptr) (string, error) {
|
||||
if procNtQueryObject == nil {
|
||||
return "", errors.New("ntdll.dll: NtQueryObject not supported")
|
||||
}
|
||||
|
||||
var buf [4 + syscall.MAX_PATH]uint16
|
||||
var result int
|
||||
r, _, e := syscall.Syscall6(procNtQueryObject.Addr(), 5,
|
||||
fd, objectNameInfo, uintptr(unsafe.Pointer(&buf)), uintptr(2*len(buf)), uintptr(unsafe.Pointer(&result)), 0)
|
||||
if r != 0 {
|
||||
return "", e
|
||||
}
|
||||
return string(utf16.Decode(buf[4 : 4+buf[0]/2])), nil
|
||||
}
|
||||
|
||||
// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2
|
||||
// terminal.
|
||||
func IsCygwinTerminal(fd uintptr) bool {
|
||||
if procGetFileInformationByHandleEx == nil {
|
||||
name, err := getFileNameByHandle(fd)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return isCygwinPipeName(name)
|
||||
}
|
||||
|
||||
// Cygwin/msys's pty is a pipe.
|
||||
ft, _, e := syscall.Syscall(procGetFileType.Addr(), 1, fd, 0, 0)
|
||||
if ft != fileTypePipe || e != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
var buf [2 + syscall.MAX_PATH]uint16
|
||||
r, _, e := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(),
|
||||
4, fd, fileNameInfo, uintptr(unsafe.Pointer(&buf)),
|
||||
uintptr(len(buf)*2), 0, 0)
|
||||
if r == 0 || e != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
l := *(*uint32)(unsafe.Pointer(&buf))
|
||||
return isCygwinPipeName(string(utf16.Decode(buf[2 : 2+l/2])))
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
module github.com/mattn/go-sqlite3
|
||||
|
||||
go 1.12
|
||||
Generated
+27
@@ -0,0 +1,27 @@
|
||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||
*.o
|
||||
*.a
|
||||
*.so
|
||||
|
||||
# Folders
|
||||
_obj
|
||||
_test
|
||||
tmp
|
||||
|
||||
# Architecture specific extensions/prefixes
|
||||
*.[568vq]
|
||||
[568vq].out
|
||||
|
||||
*.cgo1.go
|
||||
*.cgo2.c
|
||||
_cgo_defun.c
|
||||
_cgo_gotypes.go
|
||||
_cgo_export.*
|
||||
|
||||
_testmain.go
|
||||
|
||||
*.exe
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
coverage.out
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# Contributing to Zerolog
|
||||
|
||||
Thank you for your interest in contributing to **Zerolog**!
|
||||
|
||||
Zerolog is a **feature-complete**, high-performance logging library designed to be **lean** and **non-bloated**. The focus of ongoing development is on **bug fixes**, **performance improvements**, and **modernization efforts** (such as keeping up with Go best practices and compatibility with newer Go versions).
|
||||
|
||||
## What We're Looking For
|
||||
|
||||
We welcome contributions in the following areas:
|
||||
|
||||
- **Bug Fixes**: If you find an issue or unexpected behavior, please open an issue and/or submit a fix.
|
||||
- **Performance Optimizations**: Improvements that reduce memory usage, allocation count, or CPU cycles without introducing complexity are appreciated.
|
||||
- **Modernization**: Compatibility updates for newer Go versions or idiomatic improvements that do not increase library size or complexity.
|
||||
- **Documentation Enhancements**: Corrections, clarifications, and improvements to documentation or code comments.
|
||||
|
||||
## What We're *Not* Looking For
|
||||
|
||||
Zerolog is intended to remain **minimalistic and efficient**. Therefore, we are **not accepting**:
|
||||
|
||||
- New features that add optional behaviors or extend API surface area.
|
||||
- Built-in support for frameworks or external systems (e.g., bindings, integrations).
|
||||
- General-purpose abstractions or configuration helpers.
|
||||
|
||||
If you're unsure whether a change aligns with the project's philosophy, feel free to open an issue for discussion before submitting a PR.
|
||||
|
||||
## Contributing Guidelines
|
||||
|
||||
1. **Fork the repository**
|
||||
2. **Create a branch** for your fix or improvement
|
||||
3. **Write tests** to cover your changes
|
||||
4. Ensure `go test ./...` passes
|
||||
5. Run `go fmt` and `go vet` to ensure code consistency
|
||||
6. **Submit a pull request** with a clear explanation of the motivation and impact
|
||||
|
||||
## Code Style
|
||||
|
||||
- Keep the code simple, efficient, and idiomatic.
|
||||
- Avoid introducing new dependencies.
|
||||
- Preserve backwards compatibility unless explicitly discussed.
|
||||
|
||||
---
|
||||
|
||||
We appreciate your effort in helping us keep Zerolog fast, minimal, and reliable!
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2017 Olivier Poitrey
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+858
@@ -0,0 +1,858 @@
|
||||
# Zero Allocation JSON Logger
|
||||
|
||||
[](https://godoc.org/github.com/rs/zerolog) [](https://raw.githubusercontent.com/rs/zerolog/master/LICENSE) [](https://github.com/rs/zerolog/actions/workflows/test.yml) [](https://raw.githack.com/wiki/rs/zerolog/coverage.html)
|
||||
|
||||
The zerolog package provides a fast and simple logger dedicated to JSON output.
|
||||
|
||||
Zerolog's API is designed to provide both a great developer experience and stunning [performance](#benchmarks). Its unique chaining API allows zerolog to write JSON (or CBOR) log events by avoiding allocations and reflection.
|
||||
|
||||
Uber's [zap](https://godoc.org/go.uber.org/zap) library pioneered this approach. Zerolog is taking this concept to the next level with a simpler to use API and even better performance.
|
||||
|
||||
To keep the code base and the API simple, zerolog focuses on efficient structured logging only. Pretty logging on the console is made possible using the provided (but inefficient) [`zerolog.ConsoleWriter`](#pretty-logging).
|
||||
|
||||

|
||||
|
||||
## Who uses zerolog
|
||||
|
||||
Find out [who uses zerolog](https://github.com/rs/zerolog/wiki/Who-uses-zerolog) and add your company / project to the list.
|
||||
|
||||
## Features
|
||||
|
||||
- [Blazing fast](#benchmarks)
|
||||
- [Low to zero allocation](#benchmarks)
|
||||
- [Leveled logging](#leveled-logging)
|
||||
- [Sampling](#log-sampling)
|
||||
- [Hooks](#hooks)
|
||||
- [Contextual fields](#contextual-logging)
|
||||
- [`context.Context` integration](#contextcontext-integration)
|
||||
- [Integration with `net/http`](#integration-with-nethttp)
|
||||
- [JSON and CBOR encoding formats](#binary-encoding)
|
||||
- [Pretty logging for development](#pretty-logging)
|
||||
- [Error Logging (with optional Stacktrace)](#error-logging)
|
||||
- [`log/slog` integration](#integration-with-logslog)
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
go get -u github.com/rs/zerolog/log
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Simple Logging Example
|
||||
|
||||
For simple logging, import the global logger package **github.com/rs/zerolog/log**
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// UNIX Time is faster and smaller than most timestamps
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
log.Print("hello world")
|
||||
}
|
||||
|
||||
// Output: {"time":1516134303,"level":"debug","message":"hello world"}
|
||||
```
|
||||
|
||||
> Note: By default log writes to `os.Stderr`
|
||||
> Note: The default log level for `log.Print` is _trace_
|
||||
|
||||
### Contextual Logging
|
||||
|
||||
**zerolog** allows data to be added to log messages in the form of key:value pairs. The data added to the message adds "context" about the log event that can be critical for debugging as well as myriad other purposes. An example of this is below:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
log.Debug().
|
||||
Str("Scale", "833 cents").
|
||||
Float64("Interval", 833.09).
|
||||
Msg("Fibonacci is everywhere")
|
||||
|
||||
log.Debug().
|
||||
Str("Name", "Tom").
|
||||
Send()
|
||||
}
|
||||
|
||||
// Output: {"level":"debug","Scale":"833 cents","Interval":833.09,"time":1562212768,"message":"Fibonacci is everywhere"}
|
||||
// Output: {"level":"debug","Name":"Tom","time":1562212768}
|
||||
```
|
||||
|
||||
> You'll note in the above example that when adding contextual fields, the fields are strongly typed. You can find the full list of supported fields [here](#standard-types)
|
||||
|
||||
### Leveled Logging
|
||||
|
||||
#### Simple Leveled Logging Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
log.Info().Msg("hello world")
|
||||
}
|
||||
|
||||
// Output: {"time":1516134303,"level":"info","message":"hello world"}
|
||||
```
|
||||
|
||||
> It is very important to note that when using the **zerolog** chaining API, as shown above (`log.Info().Msg("hello world"`), the chain must have either the `Msg` or `Msgf` method call. If you forget to add either of these, the log will not occur and there is no compile time error to alert you of this.
|
||||
|
||||
**zerolog** allows for logging at the following levels (from highest to lowest):
|
||||
|
||||
- panic (`zerolog.PanicLevel`, 5)
|
||||
- fatal (`zerolog.FatalLevel`, 4)
|
||||
- error (`zerolog.ErrorLevel`, 3)
|
||||
- warn (`zerolog.WarnLevel`, 2)
|
||||
- info (`zerolog.InfoLevel`, 1)
|
||||
- debug (`zerolog.DebugLevel`, 0)
|
||||
- trace (`zerolog.TraceLevel`, -1)
|
||||
|
||||
You can set the Global logging level to any of these options using the `SetGlobalLevel` function in the zerolog package, passing in one of the given constants above, e.g. `zerolog.InfoLevel` would be the "info" level. Whichever level is chosen, all logs with a level greater than or equal to that level will be written. To turn off logging entirely, pass the `zerolog.Disabled` constant.
|
||||
|
||||
#### Setting Global Log Level
|
||||
|
||||
This example uses command-line flags to demonstrate various outputs depending on the chosen log level.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
debug := flag.Bool("debug", false, "sets log level to debug")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
// Default level for this example is info, unless debug flag is present
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
if *debug {
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
}
|
||||
|
||||
log.Debug().Msg("This message appears only when log level set to Debug")
|
||||
log.Info().Msg("This message appears when log level set to Debug or Info")
|
||||
|
||||
if e := log.Debug(); e.Enabled() {
|
||||
// Compute log output only if enabled.
|
||||
value := "bar"
|
||||
e.Str("foo", value).Msg("some debug message")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Info Output (no flag)
|
||||
|
||||
```bash
|
||||
$ ./logLevelExample
|
||||
{"time":1516387492,"level":"info","message":"This message appears when log level set to Debug or Info"}
|
||||
```
|
||||
|
||||
Debug Output (debug flag set)
|
||||
|
||||
```bash
|
||||
$ ./logLevelExample -debug
|
||||
{"time":1516387573,"level":"debug","message":"This message appears only when log level set to Debug"}
|
||||
{"time":1516387573,"level":"info","message":"This message appears when log level set to Debug or Info"}
|
||||
{"time":1516387573,"level":"debug","foo":"bar","message":"some debug message"}
|
||||
```
|
||||
|
||||
#### Logging without Level or Message
|
||||
|
||||
You may choose to log without a specific level by using the `Log` method. You may also write without a message by setting an empty string in the `msg string` parameter of the `Msg` method. Both are demonstrated in the example below.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
log.Log().
|
||||
Str("foo", "bar").
|
||||
Msg("")
|
||||
}
|
||||
|
||||
// Output: {"time":1494567715,"foo":"bar"}
|
||||
```
|
||||
|
||||
### Error Logging
|
||||
|
||||
You can log errors using the `Err` method
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
err := errors.New("seems we have an error here")
|
||||
log.Error().Err(err).Msg("")
|
||||
}
|
||||
|
||||
// Output: {"level":"error","error":"seems we have an error here","time":1609085256}
|
||||
```
|
||||
|
||||
> The default field name for errors is `error`, you can change this by setting `zerolog.ErrorFieldName` to meet your needs.
|
||||
|
||||
#### Error Logging with Stacktrace
|
||||
|
||||
Using `github.com/pkg/errors`, you can add a formatted stacktrace to your errors.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog/pkgerrors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
|
||||
|
||||
err := outer()
|
||||
log.Error().Stack().Err(err).Msg("")
|
||||
}
|
||||
|
||||
func inner() error {
|
||||
return errors.New("seems we have an error here")
|
||||
}
|
||||
|
||||
func middle() error {
|
||||
err := inner()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func outer() error {
|
||||
err := middle()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Output: {"level":"error","stack":[{"func":"inner","line":"20","source":"errors.go"},{"func":"middle","line":"24","source":"errors.go"},{"func":"outer","line":"32","source":"errors.go"},{"func":"main","line":"15","source":"errors.go"},{"func":"main","line":"204","source":"proc.go"},{"func":"goexit","line":"1374","source":"asm_amd64.s"}],"error":"seems we have an error here","time":1609086683}
|
||||
```
|
||||
|
||||
> zerolog.ErrorStackMarshaler must be set in order for the stack to output anything.
|
||||
|
||||
#### Logging Fatal Messages
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
err := errors.New("A repo man spends his life getting into tense situations")
|
||||
service := "myservice"
|
||||
|
||||
zerolog.TimeFieldFormat = zerolog.TimeFormatUnix
|
||||
|
||||
log.Fatal().
|
||||
Err(err).
|
||||
Str("service", service).
|
||||
Msgf("Cannot start %s", service)
|
||||
}
|
||||
|
||||
// Output: {"time":1516133263,"level":"fatal","error":"A repo man spends his life getting into tense situations","service":"myservice","message":"Cannot start myservice"}
|
||||
// exit status 1
|
||||
```
|
||||
|
||||
> NOTE: Using `Msgf` generates one allocation even when the logger is disabled.
|
||||
|
||||
### Create logger instance to manage different outputs
|
||||
|
||||
```go
|
||||
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
|
||||
logger.Info().Str("foo", "bar").Msg("hello world")
|
||||
|
||||
// Output: {"level":"info","time":1494567715,"message":"hello world","foo":"bar"}
|
||||
```
|
||||
|
||||
### Sub-loggers let you chain loggers with additional context
|
||||
|
||||
```go
|
||||
sublogger := log.With().
|
||||
Str("component", "foo").
|
||||
Logger()
|
||||
sublogger.Info().Msg("hello world")
|
||||
|
||||
// Output: {"level":"info","time":1494567715,"message":"hello world","component":"foo"}
|
||||
```
|
||||
|
||||
### Pretty logging
|
||||
|
||||
To log a human-friendly, colorized output, use `zerolog.ConsoleWriter`:
|
||||
|
||||
```go
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
|
||||
|
||||
log.Info().Str("foo", "bar").Msg("Hello world")
|
||||
|
||||
// Output: 3:04PM INF Hello World foo=bar
|
||||
```
|
||||
|
||||
To customize the configuration and formatting:
|
||||
|
||||
```go
|
||||
output := zerolog.ConsoleWriter{Out: os.Stdout, TimeFormat: time.RFC3339}
|
||||
output.FormatLevel = func(i interface{}) string {
|
||||
return strings.ToUpper(fmt.Sprintf("| %-6s|", i))
|
||||
}
|
||||
output.FormatMessage = func(i interface{}) string {
|
||||
return fmt.Sprintf("***%s****", i)
|
||||
}
|
||||
output.FormatFieldName = func(i interface{}) string {
|
||||
return fmt.Sprintf("%s:", i)
|
||||
}
|
||||
output.FormatFieldValue = func(i interface{}) string {
|
||||
return strings.ToUpper(fmt.Sprintf("%s", i))
|
||||
}
|
||||
|
||||
log := zerolog.New(output).With().Timestamp().Logger()
|
||||
|
||||
log.Info().Str("foo", "bar").Msg("Hello World")
|
||||
|
||||
// Output: 2006-01-02T15:04:05Z07:00 | INFO | ***Hello World**** foo:BAR
|
||||
```
|
||||
|
||||
To use custom advanced formatting:
|
||||
|
||||
```go
|
||||
output := zerolog.ConsoleWriter{Out: os.Stdout, NoColor: true,
|
||||
PartsOrder: []string{"level", "one", "two", "three", "message"},
|
||||
FieldsExclude: []string{"one", "two", "three"}}
|
||||
output.FormatLevel = func(i interface{}) string { return strings.ToUpper(fmt.Sprintf("%-6s", i)) }
|
||||
output.FormatFieldName = func(i interface{}) string { return fmt.Sprintf("%s:", i) }
|
||||
output.FormatPartValueByName = func(i interface{}, s string) string {
|
||||
var ret string
|
||||
switch s {
|
||||
case "one":
|
||||
ret = strings.ToUpper(fmt.Sprintf("%s", i))
|
||||
case "two":
|
||||
ret = strings.ToLower(fmt.Sprintf("%s", i))
|
||||
case "three":
|
||||
ret = strings.ToLower(fmt.Sprintf("(%s)", i))
|
||||
}
|
||||
return ret
|
||||
}
|
||||
log := zerolog.New(output)
|
||||
|
||||
log.Info().Str("foo", "bar").
|
||||
Str("two", "TEST_TWO").
|
||||
Str("one", "test_one").
|
||||
Str("three", "test_three").
|
||||
Msg("Hello World")
|
||||
|
||||
// Output: INFO TEST_ONE test_two (test_three) Hello World foo:bar
|
||||
```
|
||||
|
||||
### Sub dictionary
|
||||
|
||||
```go
|
||||
log.Info().
|
||||
Str("foo", "bar").
|
||||
Dict("dict", zerolog.Dict().
|
||||
Str("bar", "baz").
|
||||
Int("n", 1),
|
||||
).Msg("hello world")
|
||||
|
||||
// Output: {"level":"info","time":1494567715,"foo":"bar","dict":{"bar":"baz","n":1},"message":"hello world"}
|
||||
```
|
||||
|
||||
### Customize automatic field names
|
||||
|
||||
```go
|
||||
zerolog.TimestampFieldName = "t"
|
||||
zerolog.LevelFieldName = "l"
|
||||
zerolog.MessageFieldName = "m"
|
||||
|
||||
log.Info().Msg("hello world")
|
||||
|
||||
// Output: {"l":"info","t":1494567715,"m":"hello world"}
|
||||
```
|
||||
|
||||
### Add contextual fields to the global logger
|
||||
|
||||
```go
|
||||
log.Logger = log.With().Str("foo", "bar").Logger()
|
||||
```
|
||||
|
||||
### Add file and line number to log
|
||||
|
||||
Equivalent of `Llongfile`:
|
||||
|
||||
```go
|
||||
log.Logger = log.With().Caller().Logger()
|
||||
log.Info().Msg("hello world")
|
||||
|
||||
// Output: {"level": "info", "message": "hello world", "caller": "/go/src/your_project/some_file:21"}
|
||||
```
|
||||
|
||||
Equivalent of `Lshortfile`:
|
||||
|
||||
```go
|
||||
zerolog.CallerMarshalFunc = func(pc uintptr, file string, line int) string {
|
||||
return filepath.Base(file) + ":" + strconv.Itoa(line)
|
||||
}
|
||||
log.Logger = log.With().Caller().Logger()
|
||||
log.Info().Msg("hello world")
|
||||
|
||||
// Output: {"level": "info", "message": "hello world", "caller": "some_file:21"}
|
||||
```
|
||||
|
||||
### Thread-safe, lock-free, non-blocking writer
|
||||
|
||||
If your writer might be slow or not thread-safe and you need your log producers to never get slowed down by a slow writer, you can use a `diode.Writer` as follows:
|
||||
|
||||
```go
|
||||
wr := diode.NewWriter(os.Stdout, 1000, 10*time.Millisecond, func(missed int) {
|
||||
fmt.Printf("Logger Dropped %d messages", missed)
|
||||
})
|
||||
log := zerolog.New(wr)
|
||||
log.Print("test")
|
||||
```
|
||||
|
||||
You will need to install `code.cloudfoundry.org/go-diodes` to use this feature.
|
||||
|
||||
### Log Sampling
|
||||
|
||||
```go
|
||||
sampled := log.Sample(&zerolog.BasicSampler{N: 10})
|
||||
sampled.Info().Msg("will be logged every 10 messages")
|
||||
|
||||
// Output: {"time":1494567715,"level":"info","message":"will be logged every 10 messages"}
|
||||
```
|
||||
|
||||
More advanced sampling:
|
||||
|
||||
```go
|
||||
// Will let 5 debug messages per period of 1 second.
|
||||
// Over 5 debug message, 1 every 100 debug messages are logged.
|
||||
// Other levels are not sampled.
|
||||
sampled := log.Sample(zerolog.LevelSampler{
|
||||
DebugSampler: &zerolog.BurstSampler{
|
||||
Burst: 5,
|
||||
Period: 1*time.Second,
|
||||
NextSampler: &zerolog.BasicSampler{N: 100},
|
||||
},
|
||||
})
|
||||
sampled.Debug().Msg("hello world")
|
||||
|
||||
// Output: {"time":1494567715,"level":"debug","message":"hello world"}
|
||||
```
|
||||
|
||||
### Hooks
|
||||
|
||||
```go
|
||||
type SeverityHook struct{}
|
||||
|
||||
func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
|
||||
if level != zerolog.NoLevel {
|
||||
e.Str("severity", level.String())
|
||||
}
|
||||
}
|
||||
|
||||
hooked := log.Hook(SeverityHook{})
|
||||
hooked.Warn().Msg("")
|
||||
|
||||
// Output: {"level":"warn","severity":"warn"}
|
||||
```
|
||||
|
||||
### Pass a sub-logger by context
|
||||
|
||||
```go
|
||||
ctx := log.With().Str("component", "module").Logger().WithContext(ctx)
|
||||
|
||||
log.Ctx(ctx).Info().Msg("hello world")
|
||||
|
||||
// Output: {"component":"module","level":"info","message":"hello world"}
|
||||
```
|
||||
|
||||
### Set as standard logger output
|
||||
|
||||
```go
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
Str("foo", "bar").
|
||||
Logger()
|
||||
|
||||
stdlog.SetFlags(0)
|
||||
stdlog.SetOutput(log)
|
||||
|
||||
stdlog.Print("hello world")
|
||||
|
||||
// Output: {"foo":"bar","message":"hello world"}
|
||||
```
|
||||
|
||||
### context.Context integration
|
||||
|
||||
Go contexts are commonly passed throughout Go code, and this can help you pass
|
||||
your Logger into places it might otherwise be hard to inject. The `Logger`
|
||||
instance may be attached to Go context (`context.Context`) using
|
||||
`Logger.WithContext(ctx)` and extracted from it using `zerolog.Ctx(ctx)`.
|
||||
For example:
|
||||
|
||||
```go
|
||||
func f() {
|
||||
logger := zerolog.New(os.Stdout)
|
||||
ctx := context.Background()
|
||||
|
||||
// Attach the Logger to the context.Context
|
||||
ctx = logger.WithContext(ctx)
|
||||
someFunc(ctx)
|
||||
}
|
||||
|
||||
func someFunc(ctx context.Context) {
|
||||
// Get Logger from the go Context. if it's nil, then
|
||||
// `zerolog.DefaultContextLogger` is returned, if
|
||||
// `DefaultContextLogger` is nil, then a disabled logger is returned.
|
||||
logger := zerolog.Ctx(ctx)
|
||||
logger.Info().Msg("Hello")
|
||||
}
|
||||
```
|
||||
|
||||
A second form of `context.Context` integration allows you to pass the current
|
||||
`context.Context` into the logged event, and retrieve it from hooks. This can be
|
||||
useful to log trace and span IDs or other information stored in the go context,
|
||||
and facilitates the unification of logging and tracing in some systems:
|
||||
|
||||
```go
|
||||
type TracingHook struct{}
|
||||
|
||||
func (h TracingHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
|
||||
ctx := e.GetCtx()
|
||||
spanId := getSpanIdFromContext(ctx) // as per your tracing framework
|
||||
e.Str("span-id", spanId)
|
||||
}
|
||||
|
||||
func f() {
|
||||
// Setup the logger
|
||||
logger := zerolog.New(os.Stdout)
|
||||
logger = logger.Hook(TracingHook{})
|
||||
|
||||
ctx := context.Background()
|
||||
// Use the Ctx function to make the context available to the hook
|
||||
logger.Info().Ctx(ctx).Msg("Hello")
|
||||
}
|
||||
```
|
||||
|
||||
### Integration with `net/http`
|
||||
|
||||
The `github.com/rs/zerolog/hlog` package provides some helpers to integrate zerolog with `http.Handler`.
|
||||
|
||||
In this example we use [alice](https://github.com/justinas/alice) to install logger for better readability.
|
||||
|
||||
```go
|
||||
log := zerolog.New(os.Stdout).With().
|
||||
Timestamp().
|
||||
Str("role", "my-service").
|
||||
Str("host", host).
|
||||
Logger()
|
||||
|
||||
c := alice.New()
|
||||
|
||||
// Install the logger handler with default output on the console
|
||||
c = c.Append(hlog.NewHandler(log))
|
||||
|
||||
// Install some provided extra handler to set some request's context fields.
|
||||
// Thanks to that handler, all our logs will come with some prepopulated fields.
|
||||
c = c.Append(hlog.AccessHandler(func(r *http.Request, status, size int, duration time.Duration) {
|
||||
hlog.FromRequest(r).Info().
|
||||
Str("method", r.Method).
|
||||
Stringer("url", r.URL).
|
||||
Int("status", status).
|
||||
Int("size", size).
|
||||
Dur("duration", duration).
|
||||
Msg("")
|
||||
}))
|
||||
c = c.Append(hlog.RemoteAddrHandler("ip"))
|
||||
c = c.Append(hlog.UserAgentHandler("user_agent"))
|
||||
c = c.Append(hlog.RefererHandler("referer"))
|
||||
c = c.Append(hlog.RequestIDHandler("req_id", "Request-Id"))
|
||||
|
||||
// Here is your final handler
|
||||
h := c.Then(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Get the logger from the request's context. You can safely assume it
|
||||
// will be always there: if the handler is removed, hlog.FromRequest
|
||||
// will return a no-op logger.
|
||||
hlog.FromRequest(r).Info().
|
||||
Str("user", "current user").
|
||||
Str("status", "ok").
|
||||
Msg("Something happened")
|
||||
|
||||
// Output: {"level":"info","time":"2001-02-03T04:05:06Z","role":"my-service","host":"local-hostname","req_id":"b4g0l5t6tfid6dtrapu0","user":"current user","status":"ok","message":"Something happened"}
|
||||
}))
|
||||
http.Handle("/", h)
|
||||
|
||||
if err := http.ListenAndServe(":8080", nil); err != nil {
|
||||
log.Fatal().Err(err).Msg("Startup failed")
|
||||
}
|
||||
```
|
||||
|
||||
## Multiple Log Output
|
||||
|
||||
`zerolog.MultiLevelWriter` may be used to send the log message to multiple outputs.
|
||||
|
||||
In this example, we send the log message to both `os.Stdout` and the in-built `ConsoleWriter`.
|
||||
|
||||
```go
|
||||
func main() {
|
||||
consoleWriter := zerolog.ConsoleWriter{Out: os.Stdout}
|
||||
multi := zerolog.MultiLevelWriter(consoleWriter, os.Stdout)
|
||||
logger := zerolog.New(multi).With().Timestamp().Logger()
|
||||
logger.Info().Msg("Hello World!")
|
||||
}
|
||||
|
||||
// Output (Line 1: Console; Line 2: Stdout)
|
||||
// 12:36PM INF Hello World!
|
||||
// {"level":"info","time":"2019-11-07T12:36:38+03:00","message":"Hello World!"}
|
||||
```
|
||||
|
||||
## Global Settings
|
||||
|
||||
Some settings can be changed and will be applied to all loggers:
|
||||
|
||||
- `log.Logger`: You can set this value to customize the global logger (the one used by package level methods).
|
||||
- `zerolog.SetGlobalLevel`: Can raise the minimum level of all loggers. Call this with `zerolog.Disabled` to disable logging altogether (quiet mode).
|
||||
- `zerolog.DisableSampling`: If argument is `true`, all sampled loggers will stop sampling and issue 100% of their log events.
|
||||
- `zerolog.TimestampFieldName`: Can be set to customize `Timestamp` field name.
|
||||
- `zerolog.LevelFieldName`: Can be set to customize level field name.
|
||||
- `zerolog.MessageFieldName`: Can be set to customize message field name.
|
||||
- `zerolog.ErrorFieldName`: Can be set to customize `Err` field name.
|
||||
- `zerolog.TimeFieldFormat`: Can be set to customize `Time` field value formatting. If set with `zerolog.TimeFormatUnix`, `zerolog.TimeFormatUnixMs` or `zerolog.TimeFormatUnixMicro`, times are formatted as UNIX timestamp.
|
||||
- `zerolog.DurationFieldUnit`: Can be set to customize the unit for time.Duration type fields added by `Dur` (default: `time.Millisecond`).
|
||||
- `zerolog.DurationFieldFormat`: Can be set to `DurationFormatFloat`, `DurationFormatInt`, or `DurationFormatString` (default: `DurationFormatFloat`) to append the `Duration` as a `Float64`, `Int64`, or by calling `String()` (respectively).
|
||||
- `zerolog.DurationFieldInteger`: If set to `true`, `Dur` fields are formatted as integers instead of floats (default: `false`). Deprecated: Use `zerolog.DurationFieldFormat = DurationFormatInt` instead.
|
||||
- `zerolog.ErrorHandler`: Called whenever zerolog fails to write an event on its output. If not set, an error is printed on the stderr. This handler must be thread safe and non-blocking.
|
||||
- `zerolog.FloatingPointPrecision`: If set to a value other than -1, controls the number of digits when formatting float numbers in JSON. See [strconv.FormatFloat](https://pkg.go.dev/strconv#FormatFloat)
|
||||
for more details.
|
||||
|
||||
## Field Types
|
||||
|
||||
### Standard Types
|
||||
|
||||
- `Str`
|
||||
- `Bool`
|
||||
- `Int`, `Int8`, `Int16`, `Int32`, `Int64`
|
||||
- `Uint`, `Uint8`, `Uint16`, `Uint32`, `Uint64`
|
||||
- `Float32`, `Float64`
|
||||
|
||||
### Advanced Fields
|
||||
|
||||
- `Err`: Takes an `error` and renders it as a string using the `zerolog.ErrorFieldName` field name.
|
||||
- `Func`: Run a `func` only if the level is enabled.
|
||||
- `Timestamp`: Inserts a timestamp field with `zerolog.TimestampFieldName` field name, formatted using `zerolog.TimeFieldFormat`.
|
||||
- `Time`: Adds a field with time formatted with `zerolog.TimeFieldFormat`.
|
||||
- `Dur`: Adds a field with `time.Duration`.
|
||||
- `Dict`: Adds a sub-key/value as a field of the event.
|
||||
- `RawJSON`: Adds a field with an already encoded JSON (`[]byte`)
|
||||
- `Hex`: Adds a field with value formatted as a hexadecimal string (`[]byte`)
|
||||
- `Interface`: Uses reflection to marshal the type.
|
||||
- `IPAddr`: Adds a field with `net.IP`.
|
||||
- `IPPrefix`: Adds a field with `net.IPNet`.
|
||||
- `MACAddr`: Adds a field with `net.HardwareAddr`
|
||||
|
||||
Most fields are also available in the slice format (`Strs` for `[]string`, `Errs` for `[]error` etc.)
|
||||
|
||||
## Binary Encoding
|
||||
|
||||
In addition to the default JSON encoding, `zerolog` can produce binary logs using [CBOR](https://cbor.io) encoding. The choice of encoding can be decided at compile time using the build tag `binary_log` as follows:
|
||||
|
||||
```bash
|
||||
go build -tags binary_log .
|
||||
```
|
||||
|
||||
To decode binary encoded log files you can use any CBOR decoder. One has been tested to work
|
||||
with zerolog library is [CSD](https://github.com/toravir/csd/).
|
||||
|
||||
## Integration with `log/slog`
|
||||
|
||||
zerolog provides a `slog.Handler` implementation that routes `log/slog` records through a zerolog logger. This lets you use the standard library's `slog` API while keeping zerolog's performance and encoding:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
zl := log.Logger
|
||||
handler := zerolog.NewSlogHandler(zl)
|
||||
logger := slog.New(handler)
|
||||
|
||||
logger.Info("user logged in", "user", "alice", "role", "admin")
|
||||
}
|
||||
|
||||
// Output: {"level":"info","user":"alice","role":"admin","time":"...","message":"user logged in"}
|
||||
```
|
||||
|
||||
The handler supports all `slog` features including `WithAttrs`, `WithGroup`, nested groups, and `LogValuer` resolution. slog levels are mapped to zerolog levels (e.g. `slog.LevelDebug` to `zerolog.DebugLevel`).
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [grpc-zerolog](https://github.com/cheapRoc/grpc-zerolog): Implementation of `grpclog.LoggerV2` interface using `zerolog`
|
||||
- [overlog](https://github.com/Trendyol/overlog): Implementation of `Mapped Diagnostic Context` interface using `zerolog`
|
||||
- [zerologr](https://github.com/go-logr/zerologr): Implementation of `logr.LogSink` interface using `zerolog`
|
||||
- [logze](https://github.com/maxbolgarin/logze): Implementation of `log/slog` interface using `zerolog`
|
||||
|
||||
## Benchmarks
|
||||
|
||||
See [logbench](http://bench.zerolog.io/) for more comprehensive and up-to-date benchmarks.
|
||||
|
||||
All operations are allocation free (those numbers _include_ JSON encoding):
|
||||
|
||||
```text
|
||||
BenchmarkLogEmpty-8 100000000 19.1 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkDisabled-8 500000000 4.07 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkInfo-8 30000000 42.5 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkContextFields-8 30000000 44.9 ns/op 0 B/op 0 allocs/op
|
||||
BenchmarkLogFields-8 10000000 184 ns/op 0 B/op 0 allocs/op
|
||||
```
|
||||
|
||||
There are a few Go logging benchmarks and comparisons that include zerolog.
|
||||
|
||||
- [imkira/go-loggers-bench](https://github.com/imkira/go-loggers-bench)
|
||||
- [uber-common/zap](https://github.com/uber-go/zap#performance)
|
||||
|
||||
Using Uber's zap comparison benchmark:
|
||||
|
||||
Log a message and 10 fields:
|
||||
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :------------------ | :---------: | :-------------: | :---------------: |
|
||||
| zerolog | 767 ns/op | 552 B/op | 6 allocs/op |
|
||||
| :zap: zap | 848 ns/op | 704 B/op | 2 allocs/op |
|
||||
| :zap: zap (sugared) | 1363 ns/op | 1610 B/op | 20 allocs/op |
|
||||
| go-kit | 3614 ns/op | 2895 B/op | 66 allocs/op |
|
||||
| lion | 5392 ns/op | 5807 B/op | 63 allocs/op |
|
||||
| logrus | 5661 ns/op | 6092 B/op | 78 allocs/op |
|
||||
| apex/log | 15332 ns/op | 3832 B/op | 65 allocs/op |
|
||||
| log15 | 20657 ns/op | 5632 B/op | 93 allocs/op |
|
||||
|
||||
Log a message with a logger that already has 10 fields of context:
|
||||
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :------------------ | :---------: | :-------------: | :---------------: |
|
||||
| zerolog | 52 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap | 283 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
|
||||
| lion | 2702 ns/op | 4074 B/op | 38 allocs/op |
|
||||
| go-kit | 3378 ns/op | 3046 B/op | 52 allocs/op |
|
||||
| logrus | 4309 ns/op | 4564 B/op | 63 allocs/op |
|
||||
| apex/log | 13456 ns/op | 2898 B/op | 51 allocs/op |
|
||||
| log15 | 14179 ns/op | 2642 B/op | 44 allocs/op |
|
||||
|
||||
Log a static string, without any context or `printf`-style templating:
|
||||
|
||||
| Library | Time | Bytes Allocated | Objects Allocated |
|
||||
| :------------------ | :--------: | :-------------: | :---------------: |
|
||||
| zerolog | 50 ns/op | 0 B/op | 0 allocs/op |
|
||||
| :zap: zap | 236 ns/op | 0 B/op | 0 allocs/op |
|
||||
| standard library | 453 ns/op | 80 B/op | 2 allocs/op |
|
||||
| :zap: zap (sugared) | 337 ns/op | 80 B/op | 2 allocs/op |
|
||||
| go-kit | 508 ns/op | 656 B/op | 13 allocs/op |
|
||||
| lion | 771 ns/op | 1224 B/op | 10 allocs/op |
|
||||
| logrus | 1244 ns/op | 1505 B/op | 27 allocs/op |
|
||||
| apex/log | 2751 ns/op | 584 B/op | 11 allocs/op |
|
||||
| log15 | 5181 ns/op | 1592 B/op | 26 allocs/op |
|
||||
|
||||
## Caveats
|
||||
|
||||
### Field duplication
|
||||
|
||||
Note that zerolog does no de-duplication of fields. Using the same key multiple times creates multiple keys in final JSON:
|
||||
|
||||
```go
|
||||
logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
logger.Info().
|
||||
Timestamp().
|
||||
Msg("dup")
|
||||
// Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}
|
||||
```
|
||||
|
||||
In this case, many consumers will take the last value, but this is not guaranteed; check yours if in doubt.
|
||||
|
||||
### Concurrency safety
|
||||
|
||||
Be careful when calling `UpdateContext`. It is not concurrency safe. Use the `With()` method to create a child logger:
|
||||
|
||||
```go
|
||||
func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// Create a child logger for concurrency safety
|
||||
logger := log.Logger.With().Logger()
|
||||
|
||||
// Add context fields, for example User-Agent from HTTP headers
|
||||
logger.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
...
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
The `Event` object returned from the `Logger` level-specific message functions (e.g. `Log()`, `Trace()`, `Debug()`, etc.)
|
||||
is allocated in `sync.Pool` memory that will be returned to the pool as soon as the `Msg()`, `Msgf()`, `Send()`,
|
||||
or `MsgFunc()` writes the message and **must not** be accessed afterwards.
|
||||
|
||||
**Do not** hold a reference to the `*Event` while in callback functions or your own code. This is especially important in
|
||||
`Hook.Run()` and `HookFunc` functions or `MarshalZerologObject(e *Event)` callback (e.g. `LogObjectMarshaler` implementations).
|
||||
|
||||
Any `Array` objects returned from `Context.CreateArray()` or `Event.CreateArray()` are from a `sync.Pool` so **do not** hold
|
||||
references to them from within any `MarshalZerologArray(a *Array)` callback (e.g. `LogArrayMarshaler` implementations) or your
|
||||
own code as they will be cleared and returned to the pool after being buffered by a call to `Context.Array()` or `Event.Array()`.
|
||||
|
||||
Any _dictionary_ `Event` returned from `Context.CreateDict()` or `Event.CreateDict()` **must not** be referenced after being
|
||||
buffered by a call to `Array.Dict()`, `Context.Dict()`, or `Event.Dict()` as they will be cleared and returned to the pool.
|
||||
+276
@@ -0,0 +1,276 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var arrayPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &Array{
|
||||
buf: make([]byte, 0, 500),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Array is used to prepopulate an array of items
|
||||
// which can be re-used to add to log messages.
|
||||
type Array struct {
|
||||
buf []byte
|
||||
stack bool // enable error stack trace
|
||||
ctx context.Context // Optional Go context
|
||||
ch []Hook // hooks
|
||||
}
|
||||
|
||||
func putArray(a *Array) {
|
||||
// prevent any subsequent use of the Array contextual state and truncate the buffer
|
||||
a.stack = false
|
||||
a.ctx = nil
|
||||
a.ch = nil
|
||||
a.buf = a.buf[:0]
|
||||
|
||||
// Proper usage of a sync.Pool requires each entry to have approximately
|
||||
// the same memory cost. To obtain this property when the stored type
|
||||
// contains a variably-sized buffer, we add a hard limit on the maximum buffer
|
||||
// to place back in the pool.
|
||||
//
|
||||
// See https://golang.org/issue/23199
|
||||
const maxSize = 1 << 16 // 64KiB
|
||||
if cap(a.buf) <= maxSize {
|
||||
arrayPool.Put(a)
|
||||
}
|
||||
}
|
||||
|
||||
// Arr creates an array to be added to an Event or Context.
|
||||
// WARNING: This function is deprecated because it does not preserve
|
||||
// the stack, hooks, and context from the parent event.
|
||||
// Deprecated: Use Event.CreateArray or Context.CreateArray instead.
|
||||
func Arr() *Array {
|
||||
a := arrayPool.Get().(*Array)
|
||||
a.buf = a.buf[:0]
|
||||
a.stack = false
|
||||
a.ctx = nil
|
||||
a.ch = nil
|
||||
return a
|
||||
}
|
||||
|
||||
// MarshalZerologArray method here is no-op - since data is
|
||||
// already in the needed format.
|
||||
func (*Array) MarshalZerologArray(*Array) {
|
||||
// untestable: there's no code to be covered
|
||||
}
|
||||
|
||||
func (a *Array) write(dst []byte) []byte {
|
||||
dst = enc.AppendArrayStart(dst)
|
||||
if len(a.buf) > 0 {
|
||||
dst = append(dst, a.buf...)
|
||||
}
|
||||
dst = enc.AppendArrayEnd(dst)
|
||||
putArray(a)
|
||||
return dst
|
||||
}
|
||||
|
||||
// Object marshals an object that implement the LogObjectMarshaler
|
||||
// interface and appends it to the array.
|
||||
func (a *Array) Object(obj LogObjectMarshaler) *Array {
|
||||
a.buf = appendObject(enc.AppendArrayDelim(a.buf), obj, a.stack, a.ctx, a.ch)
|
||||
return a
|
||||
}
|
||||
|
||||
// Str appends the val as a string to the array.
|
||||
func (a *Array) Str(val string) *Array {
|
||||
a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), val)
|
||||
return a
|
||||
}
|
||||
|
||||
// Bytes appends the val as a string to the array.
|
||||
func (a *Array) Bytes(val []byte) *Array {
|
||||
a.buf = enc.AppendBytes(enc.AppendArrayDelim(a.buf), val)
|
||||
return a
|
||||
}
|
||||
|
||||
// Hex appends the val as a hex string to the array.
|
||||
func (a *Array) Hex(val []byte) *Array {
|
||||
a.buf = enc.AppendHex(enc.AppendArrayDelim(a.buf), val)
|
||||
return a
|
||||
}
|
||||
|
||||
// RawJSON adds already encoded JSON to the array.
|
||||
func (a *Array) RawJSON(val []byte) *Array {
|
||||
a.buf = appendJSON(enc.AppendArrayDelim(a.buf), val)
|
||||
return a
|
||||
}
|
||||
|
||||
// Err serializes and appends the err to the array.
|
||||
func (a *Array) Err(err error) *Array {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
a.buf = enc.AppendNil(enc.AppendArrayDelim(a.buf))
|
||||
case LogObjectMarshaler:
|
||||
a = a.Object(m)
|
||||
case error:
|
||||
if !isNilValue(m) {
|
||||
a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m.Error())
|
||||
}
|
||||
case string:
|
||||
a.buf = enc.AppendString(enc.AppendArrayDelim(a.buf), m)
|
||||
default:
|
||||
a.buf = enc.AppendInterface(enc.AppendArrayDelim(a.buf), m)
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
// Errs serializes and appends errors to the array.
|
||||
func (a *Array) Errs(errs []error) *Array {
|
||||
for _, err := range errs {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
a = a.Interface(nil)
|
||||
case LogObjectMarshaler:
|
||||
a = a.Object(m)
|
||||
case error:
|
||||
if !isNilValue(m) {
|
||||
a = a.Str(m.Error())
|
||||
}
|
||||
case string:
|
||||
a = a.Str(m)
|
||||
default:
|
||||
a = a.Interface(m)
|
||||
}
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Bool appends the val as a bool to the array.
|
||||
func (a *Array) Bool(b bool) *Array {
|
||||
a.buf = enc.AppendBool(enc.AppendArrayDelim(a.buf), b)
|
||||
return a
|
||||
}
|
||||
|
||||
// Int appends i as a int to the array.
|
||||
func (a *Array) Int(i int) *Array {
|
||||
a.buf = enc.AppendInt(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Int8 appends i as a int8 to the array.
|
||||
func (a *Array) Int8(i int8) *Array {
|
||||
a.buf = enc.AppendInt8(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Int16 appends i as a int16 to the array.
|
||||
func (a *Array) Int16(i int16) *Array {
|
||||
a.buf = enc.AppendInt16(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Int32 appends i as a int32 to the array.
|
||||
func (a *Array) Int32(i int32) *Array {
|
||||
a.buf = enc.AppendInt32(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Int64 appends i as a int64 to the array.
|
||||
func (a *Array) Int64(i int64) *Array {
|
||||
a.buf = enc.AppendInt64(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Uint appends i as a uint to the array.
|
||||
func (a *Array) Uint(i uint) *Array {
|
||||
a.buf = enc.AppendUint(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Uint8 appends i as a uint8 to the array.
|
||||
func (a *Array) Uint8(i uint8) *Array {
|
||||
a.buf = enc.AppendUint8(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Uint16 appends i as a uint16 to the array.
|
||||
func (a *Array) Uint16(i uint16) *Array {
|
||||
a.buf = enc.AppendUint16(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Uint32 appends i as a uint32 to the array.
|
||||
func (a *Array) Uint32(i uint32) *Array {
|
||||
a.buf = enc.AppendUint32(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Uint64 appends i as a uint64 to the array.
|
||||
func (a *Array) Uint64(i uint64) *Array {
|
||||
a.buf = enc.AppendUint64(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// Float32 appends f as a float32 to the array.
|
||||
func (a *Array) Float32(f float32) *Array {
|
||||
a.buf = enc.AppendFloat32(enc.AppendArrayDelim(a.buf), f, FloatingPointPrecision)
|
||||
return a
|
||||
}
|
||||
|
||||
// Float64 appends f as a float64 to the array.
|
||||
func (a *Array) Float64(f float64) *Array {
|
||||
a.buf = enc.AppendFloat64(enc.AppendArrayDelim(a.buf), f, FloatingPointPrecision)
|
||||
return a
|
||||
}
|
||||
|
||||
// Time appends t formatted as string using zerolog.TimeFieldFormat.
|
||||
func (a *Array) Time(t time.Time) *Array {
|
||||
a.buf = enc.AppendTime(enc.AppendArrayDelim(a.buf), t, TimeFieldFormat)
|
||||
return a
|
||||
}
|
||||
|
||||
// Dur appends d to the array.
|
||||
func (a *Array) Dur(d time.Duration) *Array {
|
||||
a.buf = enc.AppendDuration(enc.AppendArrayDelim(a.buf), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return a
|
||||
}
|
||||
|
||||
// Interface appends i marshaled using reflection.
|
||||
func (a *Array) Interface(i interface{}) *Array {
|
||||
if obj, ok := i.(LogObjectMarshaler); ok {
|
||||
return a.Object(obj)
|
||||
}
|
||||
a.buf = enc.AppendInterface(enc.AppendArrayDelim(a.buf), i)
|
||||
return a
|
||||
}
|
||||
|
||||
// IPAddr adds a net.IP IPv4 or IPv6 address to the array
|
||||
func (a *Array) IPAddr(ip net.IP) *Array {
|
||||
a.buf = enc.AppendIPAddr(enc.AppendArrayDelim(a.buf), ip)
|
||||
return a
|
||||
}
|
||||
|
||||
// IPPrefix adds a net.IPNet IPv4 or IPv6 Prefix (IP + mask) to the array
|
||||
func (a *Array) IPPrefix(pfx net.IPNet) *Array {
|
||||
a.buf = enc.AppendIPPrefix(enc.AppendArrayDelim(a.buf), pfx)
|
||||
return a
|
||||
}
|
||||
|
||||
// MACAddr adds a net.HardwareAddr MAC (Ethernet) address to the array
|
||||
func (a *Array) MACAddr(ha net.HardwareAddr) *Array {
|
||||
a.buf = enc.AppendMACAddr(enc.AppendArrayDelim(a.buf), ha)
|
||||
return a
|
||||
}
|
||||
|
||||
// Dict adds the dict Event to the array
|
||||
func (a *Array) Dict(dict *Event) *Array {
|
||||
dict.buf = enc.AppendEndMarker(dict.buf)
|
||||
a.buf = append(enc.AppendArrayDelim(a.buf), dict.buf...)
|
||||
putEvent(dict)
|
||||
return a
|
||||
}
|
||||
|
||||
// Type adds the val's type using reflection to the array.
|
||||
func (a *Array) Type(val interface{}) *Array {
|
||||
a.buf = enc.AppendType(enc.AppendArrayDelim(a.buf), val)
|
||||
return a
|
||||
}
|
||||
+535
@@ -0,0 +1,535 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/mattn/go-colorable"
|
||||
)
|
||||
|
||||
const (
|
||||
colorBlack = iota + 30
|
||||
colorRed
|
||||
colorGreen
|
||||
colorYellow
|
||||
colorBlue
|
||||
colorMagenta
|
||||
colorCyan
|
||||
colorWhite
|
||||
|
||||
colorBold = 1
|
||||
colorDarkGray = 90
|
||||
|
||||
unknownLevel = "???"
|
||||
)
|
||||
|
||||
var (
|
||||
consoleBufPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return bytes.NewBuffer(make([]byte, 0, 100))
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
consoleDefaultTimeFormat = time.Kitchen
|
||||
)
|
||||
|
||||
// Formatter transforms the input into a formatted string.
|
||||
type Formatter func(interface{}) string
|
||||
|
||||
// FormatterByFieldName transforms the input into a formatted string,
|
||||
// being able to differentiate formatting based on field name.
|
||||
type FormatterByFieldName func(interface{}, string) string
|
||||
|
||||
// ConsoleWriter parses the JSON input and writes it in an
|
||||
// (optionally) colorized, human-friendly format to Out.
|
||||
type ConsoleWriter struct {
|
||||
// Out is the output destination.
|
||||
Out io.Writer
|
||||
|
||||
// NoColor disables the colorized output.
|
||||
NoColor bool
|
||||
|
||||
// TimeFormat specifies the format for timestamp in output.
|
||||
TimeFormat string
|
||||
|
||||
// TimeLocation tells ConsoleWriter’s default FormatTimestamp
|
||||
// how to localize the time.
|
||||
TimeLocation *time.Location
|
||||
|
||||
// PartsOrder defines the order of parts in output.
|
||||
PartsOrder []string
|
||||
|
||||
// PartsExclude defines parts to not display in output.
|
||||
PartsExclude []string
|
||||
|
||||
// FieldsOrder defines the order of contextual fields in output.
|
||||
FieldsOrder []string
|
||||
|
||||
fieldIsOrdered map[string]int
|
||||
|
||||
// FieldsExclude defines contextual fields to not display in output.
|
||||
FieldsExclude []string
|
||||
|
||||
FormatTimestamp Formatter
|
||||
FormatLevel Formatter
|
||||
FormatCaller Formatter
|
||||
FormatMessage Formatter
|
||||
FormatFieldName Formatter
|
||||
FormatFieldValue Formatter
|
||||
FormatErrFieldName Formatter
|
||||
FormatErrFieldValue Formatter
|
||||
// If this is configured it is used for "part" values and
|
||||
// has precedence on FormatFieldValue
|
||||
FormatPartValueByName FormatterByFieldName
|
||||
|
||||
FormatExtra func(map[string]interface{}, *bytes.Buffer) error
|
||||
|
||||
FormatPrepare func(map[string]interface{}) error
|
||||
}
|
||||
|
||||
// NewConsoleWriter creates and initializes a new ConsoleWriter.
|
||||
func NewConsoleWriter(options ...func(w *ConsoleWriter)) ConsoleWriter {
|
||||
w := ConsoleWriter{
|
||||
Out: os.Stdout,
|
||||
TimeFormat: consoleDefaultTimeFormat,
|
||||
PartsOrder: consoleDefaultPartsOrder(),
|
||||
}
|
||||
|
||||
for _, opt := range options {
|
||||
opt(&w)
|
||||
}
|
||||
|
||||
// Fix color on Windows
|
||||
if w.Out == os.Stdout || w.Out == os.Stderr {
|
||||
w.Out = colorable.NewColorable(w.Out.(*os.File))
|
||||
}
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
// Write transforms the JSON input with formatters and appends to w.Out.
|
||||
func (w ConsoleWriter) Write(p []byte) (n int, err error) {
|
||||
// Fix color on Windows
|
||||
if w.Out == os.Stdout || w.Out == os.Stderr {
|
||||
w.Out = colorable.NewColorable(w.Out.(*os.File))
|
||||
}
|
||||
|
||||
if w.PartsOrder == nil {
|
||||
w.PartsOrder = consoleDefaultPartsOrder()
|
||||
}
|
||||
|
||||
var buf = consoleBufPool.Get().(*bytes.Buffer)
|
||||
defer func() {
|
||||
buf.Reset()
|
||||
consoleBufPool.Put(buf)
|
||||
}()
|
||||
|
||||
var evt map[string]interface{}
|
||||
p = decodeIfBinaryToBytes(p)
|
||||
d := json.NewDecoder(bytes.NewReader(p))
|
||||
d.UseNumber()
|
||||
err = d.Decode(&evt)
|
||||
if err != nil {
|
||||
return n, fmt.Errorf("cannot decode event: %s", err)
|
||||
}
|
||||
|
||||
if w.FormatPrepare != nil {
|
||||
err = w.FormatPrepare(evt)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, p := range w.PartsOrder {
|
||||
w.writePart(buf, evt, p)
|
||||
}
|
||||
|
||||
w.writeFields(evt, buf)
|
||||
|
||||
if w.FormatExtra != nil {
|
||||
err = w.FormatExtra(evt, buf)
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
|
||||
err = buf.WriteByte('\n')
|
||||
if err != nil {
|
||||
return n, err
|
||||
}
|
||||
|
||||
_, err = buf.WriteTo(w.Out)
|
||||
return len(p), err
|
||||
}
|
||||
|
||||
// Call the underlying writer's Close method if it is an io.Closer. Otherwise
|
||||
// does nothing.
|
||||
func (w ConsoleWriter) Close() error {
|
||||
if closer, ok := w.Out.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeFields appends formatted key-value pairs to buf.
|
||||
func (w ConsoleWriter) writeFields(evt map[string]interface{}, buf *bytes.Buffer) {
|
||||
var fields = make([]string, 0, len(evt))
|
||||
for field := range evt {
|
||||
var isExcluded bool
|
||||
for _, excluded := range w.FieldsExclude {
|
||||
if field == excluded {
|
||||
isExcluded = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if isExcluded {
|
||||
continue
|
||||
}
|
||||
|
||||
switch field {
|
||||
case LevelFieldName, TimestampFieldName, MessageFieldName, CallerFieldName:
|
||||
continue
|
||||
}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
|
||||
if len(w.FieldsOrder) > 0 {
|
||||
w.orderFields(fields)
|
||||
} else {
|
||||
sort.Strings(fields)
|
||||
}
|
||||
|
||||
// Write space only if something has already been written to the buffer, and if there are fields.
|
||||
if buf.Len() > 0 && len(fields) > 0 {
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
|
||||
// Move the "error" field to the front
|
||||
ei := sort.Search(len(fields), func(i int) bool { return fields[i] >= ErrorFieldName })
|
||||
if ei < len(fields) && fields[ei] == ErrorFieldName {
|
||||
fields[ei] = ""
|
||||
fields = append([]string{ErrorFieldName}, fields...)
|
||||
var xfields = make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
if field == "" { // Skip empty fields
|
||||
continue
|
||||
}
|
||||
xfields = append(xfields, field)
|
||||
}
|
||||
fields = xfields
|
||||
}
|
||||
|
||||
for i, field := range fields {
|
||||
var fn Formatter
|
||||
var fv Formatter
|
||||
|
||||
if field == ErrorFieldName {
|
||||
if w.FormatErrFieldName == nil {
|
||||
fn = consoleDefaultFormatErrFieldName(w.NoColor)
|
||||
} else {
|
||||
fn = w.FormatErrFieldName
|
||||
}
|
||||
|
||||
if w.FormatErrFieldValue == nil {
|
||||
fv = consoleDefaultFormatErrFieldValue(w.NoColor)
|
||||
} else {
|
||||
fv = w.FormatErrFieldValue
|
||||
}
|
||||
} else {
|
||||
if w.FormatFieldName == nil {
|
||||
fn = consoleDefaultFormatFieldName(w.NoColor)
|
||||
} else {
|
||||
fn = w.FormatFieldName
|
||||
}
|
||||
|
||||
if w.FormatFieldValue == nil {
|
||||
fv = consoleDefaultFormatFieldValue
|
||||
} else {
|
||||
fv = w.FormatFieldValue
|
||||
}
|
||||
}
|
||||
|
||||
buf.WriteString(fn(field))
|
||||
|
||||
switch fValue := evt[field].(type) {
|
||||
case string:
|
||||
if needsQuote(fValue) {
|
||||
buf.WriteString(fv(strconv.Quote(fValue)))
|
||||
} else {
|
||||
buf.WriteString(fv(fValue))
|
||||
}
|
||||
case json.Number:
|
||||
buf.WriteString(fv(fValue))
|
||||
default:
|
||||
b, err := InterfaceMarshalFunc(fValue)
|
||||
if err != nil {
|
||||
fmt.Fprintf(buf, colorize("[error: %v]", colorRed, w.NoColor), err)
|
||||
} else {
|
||||
fmt.Fprint(buf, fv(b))
|
||||
}
|
||||
}
|
||||
|
||||
if i < len(fields)-1 { // Skip space for last field
|
||||
buf.WriteByte(' ')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// writePart appends a formatted part to buf.
|
||||
func (w ConsoleWriter) writePart(buf *bytes.Buffer, evt map[string]interface{}, p string) {
|
||||
var f Formatter
|
||||
var fvn FormatterByFieldName
|
||||
|
||||
if len(w.PartsExclude) > 0 {
|
||||
for _, exclude := range w.PartsExclude {
|
||||
if exclude == p {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch p {
|
||||
case LevelFieldName:
|
||||
if w.FormatLevel == nil {
|
||||
f = consoleDefaultFormatLevel(w.NoColor)
|
||||
} else {
|
||||
f = w.FormatLevel
|
||||
}
|
||||
case TimestampFieldName:
|
||||
if w.FormatTimestamp == nil {
|
||||
f = consoleDefaultFormatTimestamp(w.TimeFormat, w.TimeLocation, w.NoColor)
|
||||
} else {
|
||||
f = w.FormatTimestamp
|
||||
}
|
||||
case MessageFieldName:
|
||||
if w.FormatMessage == nil {
|
||||
f = consoleDefaultFormatMessage(w.NoColor, evt[LevelFieldName])
|
||||
} else {
|
||||
f = w.FormatMessage
|
||||
}
|
||||
case CallerFieldName:
|
||||
if w.FormatCaller == nil {
|
||||
f = consoleDefaultFormatCaller(w.NoColor)
|
||||
} else {
|
||||
f = w.FormatCaller
|
||||
}
|
||||
default:
|
||||
if w.FormatPartValueByName != nil {
|
||||
fvn = w.FormatPartValueByName
|
||||
} else if w.FormatFieldValue != nil {
|
||||
f = w.FormatFieldValue
|
||||
} else {
|
||||
f = consoleDefaultFormatFieldValue
|
||||
}
|
||||
}
|
||||
|
||||
var s string
|
||||
if f == nil {
|
||||
s = fvn(evt[p], p)
|
||||
} else {
|
||||
s = f(evt[p])
|
||||
}
|
||||
|
||||
if len(s) > 0 {
|
||||
if buf.Len() > 0 {
|
||||
buf.WriteByte(' ') // Write space only if not the first part
|
||||
}
|
||||
buf.WriteString(s)
|
||||
}
|
||||
}
|
||||
|
||||
// orderFields takes an array of field names and an array representing field order
|
||||
// and returns an array with any ordered fields at the beginning, in order,
|
||||
// and the remaining fields after in their original order.
|
||||
func (w ConsoleWriter) orderFields(fields []string) {
|
||||
if w.fieldIsOrdered == nil {
|
||||
w.fieldIsOrdered = make(map[string]int)
|
||||
for i, fieldName := range w.FieldsOrder {
|
||||
w.fieldIsOrdered[fieldName] = i
|
||||
}
|
||||
}
|
||||
sort.Slice(fields, func(i, j int) bool {
|
||||
ii, iOrdered := w.fieldIsOrdered[fields[i]]
|
||||
jj, jOrdered := w.fieldIsOrdered[fields[j]]
|
||||
if iOrdered && jOrdered {
|
||||
return ii < jj
|
||||
}
|
||||
if iOrdered {
|
||||
return true
|
||||
}
|
||||
if jOrdered {
|
||||
return false
|
||||
}
|
||||
return fields[i] < fields[j]
|
||||
})
|
||||
}
|
||||
|
||||
// needsQuote returns true when the string s should be quoted in output.
|
||||
func needsQuote(s string) bool {
|
||||
for i := range s {
|
||||
if s[i] < 0x20 || s[i] > 0x7e || s[i] == ' ' || s[i] == '\\' || s[i] == '"' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// colorize returns the string s wrapped in ANSI code c, unless disabled is true or c is 0.
|
||||
func colorize(s interface{}, c int, disabled bool) string {
|
||||
e := os.Getenv("NO_COLOR")
|
||||
if e != "" || c == 0 {
|
||||
disabled = true
|
||||
}
|
||||
|
||||
if disabled {
|
||||
return fmt.Sprintf("%s", s)
|
||||
}
|
||||
return fmt.Sprintf("\x1b[%dm%v\x1b[0m", c, s)
|
||||
}
|
||||
|
||||
// ----- DEFAULT FORMATTERS ---------------------------------------------------
|
||||
|
||||
func consoleDefaultPartsOrder() []string {
|
||||
return []string{
|
||||
TimestampFieldName,
|
||||
LevelFieldName,
|
||||
CallerFieldName,
|
||||
MessageFieldName,
|
||||
}
|
||||
}
|
||||
|
||||
func consoleDefaultFormatTimestamp(timeFormat string, location *time.Location, noColor bool) Formatter {
|
||||
if timeFormat == "" {
|
||||
timeFormat = consoleDefaultTimeFormat
|
||||
}
|
||||
if location == nil {
|
||||
location = time.Local
|
||||
}
|
||||
|
||||
return func(i interface{}) string {
|
||||
t := "<nil>"
|
||||
switch tt := i.(type) {
|
||||
case string:
|
||||
ts, err := time.ParseInLocation(TimeFieldFormat, tt, location)
|
||||
if err != nil {
|
||||
t = tt
|
||||
} else {
|
||||
t = ts.In(location).Format(timeFormat)
|
||||
}
|
||||
case json.Number:
|
||||
i, err := tt.Int64()
|
||||
if err != nil {
|
||||
t = tt.String()
|
||||
} else {
|
||||
var sec, nsec int64
|
||||
|
||||
switch TimeFieldFormat {
|
||||
case TimeFormatUnixNano:
|
||||
sec, nsec = 0, i
|
||||
case TimeFormatUnixMicro:
|
||||
sec, nsec = 0, int64(time.Duration(i)*time.Microsecond)
|
||||
case TimeFormatUnixMs:
|
||||
sec, nsec = 0, int64(time.Duration(i)*time.Millisecond)
|
||||
default:
|
||||
sec, nsec = i, 0
|
||||
}
|
||||
|
||||
ts := time.Unix(sec, nsec)
|
||||
t = ts.In(location).Format(timeFormat)
|
||||
}
|
||||
}
|
||||
return colorize(t, colorDarkGray, noColor)
|
||||
}
|
||||
}
|
||||
|
||||
func stripLevel(ll string) string {
|
||||
if len(ll) == 0 {
|
||||
return unknownLevel
|
||||
}
|
||||
if len(ll) > 3 {
|
||||
ll = ll[:3]
|
||||
}
|
||||
return strings.ToUpper(ll)
|
||||
}
|
||||
|
||||
func consoleDefaultFormatLevel(noColor bool) Formatter {
|
||||
return func(i interface{}) string {
|
||||
if ll, ok := i.(string); ok {
|
||||
level, _ := ParseLevel(ll)
|
||||
fl, ok := FormattedLevels[level]
|
||||
if ok {
|
||||
return colorize(fl, LevelColors[level], noColor)
|
||||
}
|
||||
return stripLevel(ll)
|
||||
}
|
||||
if i == nil {
|
||||
return unknownLevel
|
||||
}
|
||||
return stripLevel(fmt.Sprintf("%s", i))
|
||||
}
|
||||
}
|
||||
|
||||
func consoleDefaultFormatCaller(noColor bool) Formatter {
|
||||
return func(i interface{}) string {
|
||||
var c string
|
||||
if cc, ok := i.(string); ok {
|
||||
c = cc
|
||||
}
|
||||
if len(c) > 0 {
|
||||
if cwd, err := os.Getwd(); err == nil {
|
||||
if rel, err := filepath.Rel(cwd, c); err == nil {
|
||||
c = rel
|
||||
}
|
||||
}
|
||||
c = colorize(c, colorBold, noColor) + colorize(" >", colorCyan, noColor)
|
||||
}
|
||||
return c
|
||||
}
|
||||
}
|
||||
|
||||
func consoleDefaultFormatMessage(noColor bool, level interface{}) Formatter {
|
||||
return func(i interface{}) string {
|
||||
if i == nil || i == "" {
|
||||
return ""
|
||||
}
|
||||
switch level {
|
||||
case LevelInfoValue, LevelWarnValue, LevelErrorValue, LevelFatalValue, LevelPanicValue:
|
||||
return colorize(fmt.Sprintf("%s", i), colorBold, noColor)
|
||||
default:
|
||||
return fmt.Sprintf("%s", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func consoleDefaultFormatFieldName(noColor bool) Formatter {
|
||||
return func(i interface{}) string {
|
||||
return colorize(fmt.Sprintf("%s=", i), colorCyan, noColor)
|
||||
}
|
||||
}
|
||||
|
||||
func consoleDefaultFormatFieldValue(i interface{}) string {
|
||||
return fmt.Sprintf("%s", i)
|
||||
}
|
||||
|
||||
func consoleDefaultFormatErrFieldName(noColor bool) Formatter {
|
||||
return func(i interface{}) string {
|
||||
return colorize(fmt.Sprintf("%s=", i), colorCyan, noColor)
|
||||
}
|
||||
}
|
||||
|
||||
func consoleDefaultFormatErrFieldValue(noColor bool) Formatter {
|
||||
return func(i interface{}) string {
|
||||
return colorize(colorize(fmt.Sprintf("%s", i), colorBold, noColor), colorRed, noColor)
|
||||
}
|
||||
}
|
||||
+535
@@ -0,0 +1,535 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Context configures a new sub-logger with contextual fields.
|
||||
type Context struct {
|
||||
l Logger
|
||||
}
|
||||
|
||||
// Logger returns the logger with the context previously set.
|
||||
func (c Context) Logger() Logger {
|
||||
return c.l
|
||||
}
|
||||
|
||||
// Fields is a helper function to use a map or slice to set fields using type assertion.
|
||||
// Only map[string]interface{} and []interface{} are accepted. []interface{} must
|
||||
// alternate string keys and arbitrary values, and extraneous ones are ignored.
|
||||
func (c Context) Fields(fields interface{}) Context {
|
||||
c.l.context = appendFields(c.l.context, fields, c.l.stack, c.l.ctx, c.l.hooks)
|
||||
return c
|
||||
}
|
||||
|
||||
// Dict adds the field key with the dict to the logger context.
|
||||
func (c Context) Dict(key string, dict *Event) Context {
|
||||
dict.buf = enc.AppendEndMarker(dict.buf)
|
||||
c.l.context = append(enc.AppendKey(c.l.context, key), dict.buf...)
|
||||
putEvent(dict)
|
||||
return c
|
||||
}
|
||||
|
||||
// CreateDict creates an Event to be used with the Context.Dict method.
|
||||
// It preserves the stack, hooks, and context from the logger.
|
||||
// Call usual field methods like Str, Int etc to add fields to this
|
||||
// event and give it as argument the Context.Dict method.
|
||||
func (c Context) CreateDict() *Event {
|
||||
return newEvent(nil, DebugLevel, c.l.stack, c.l.ctx, c.l.hooks)
|
||||
}
|
||||
|
||||
// CreateArray creates an Array to be used with the Context.Array method.
|
||||
// It preserves the stack, hooks, and context from the logger.
|
||||
// Call usual field methods like Str, Int etc to add elements to this
|
||||
// array and give it as argument the Context.Array method.
|
||||
func (c Context) CreateArray() *Array {
|
||||
a := Arr()
|
||||
a.stack = c.l.stack
|
||||
a.ctx = c.l.ctx
|
||||
a.ch = c.l.hooks
|
||||
return a
|
||||
}
|
||||
|
||||
// Array adds the field key with an array to the event context.
|
||||
// Use c.CreateArray() to create the array or pass a type that
|
||||
// implement the LogArrayMarshaler interface.
|
||||
func (c Context) Array(key string, arr LogArrayMarshaler) Context {
|
||||
c.l.context = enc.AppendKey(c.l.context, key)
|
||||
if arr, ok := arr.(*Array); ok {
|
||||
c.l.context = arr.write(c.l.context)
|
||||
return c
|
||||
}
|
||||
a := c.CreateArray()
|
||||
arr.MarshalZerologArray(a)
|
||||
c.l.context = a.write(c.l.context)
|
||||
return c
|
||||
}
|
||||
|
||||
// Object marshals an object that implement the LogObjectMarshaler interface.
|
||||
func (c Context) Object(key string, obj LogObjectMarshaler) Context {
|
||||
e := c.l.scratchEvent()
|
||||
e.Object(key, obj)
|
||||
c.l.context = enc.AppendObjectData(c.l.context, e.buf)
|
||||
putEvent(e)
|
||||
return c
|
||||
}
|
||||
|
||||
// Objects adds the field key with objs to the logger context as an array of
|
||||
// objects that implement the LogObjectMarshaler interface.
|
||||
//
|
||||
// This is the array version that accepts a slice of LogObjectMarshaler objects.
|
||||
func (c Context) Objects(key string, objs []LogObjectMarshaler) Context {
|
||||
e := c.l.scratchEvent()
|
||||
e.Objects(key, objs)
|
||||
c.l.context = enc.AppendObjectData(c.l.context, e.buf)
|
||||
putEvent(e)
|
||||
return c
|
||||
}
|
||||
|
||||
// ObjectsV adds the field key with objs to the logger context as an array of
|
||||
// objects that implement the LogObjectMarshaler interface.
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual LogObjectMarshaler objects.
|
||||
func (c Context) ObjectsV(key string, objs ...LogObjectMarshaler) Context {
|
||||
return c.Objects(key, objs)
|
||||
}
|
||||
|
||||
// EmbedObject marshals and Embeds an object that implement the LogObjectMarshaler interface.
|
||||
func (c Context) EmbedObject(obj LogObjectMarshaler) Context {
|
||||
e := c.l.scratchEvent()
|
||||
e.EmbedObject(obj)
|
||||
c.l.context = enc.AppendObjectData(c.l.context, e.buf)
|
||||
putEvent(e)
|
||||
return c
|
||||
}
|
||||
|
||||
// Str adds the field key with val as a string to the logger context.
|
||||
func (c Context) Str(key, val string) Context {
|
||||
c.l.context = enc.AppendString(enc.AppendKey(c.l.context, key), val)
|
||||
return c
|
||||
}
|
||||
|
||||
// Strs adds the field key with val as a string to the logger context.
|
||||
//
|
||||
// This is the array version that accepts a slice of string values.
|
||||
func (c Context) Strs(key string, vals []string) Context {
|
||||
c.l.context = enc.AppendStrings(enc.AppendKey(c.l.context, key), vals)
|
||||
return c
|
||||
}
|
||||
|
||||
// StrsV adds the field key with vals as a []string to the logger context.
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual strings.
|
||||
func (c Context) StrsV(key string, vals ...string) Context {
|
||||
return c.Strs(key, vals)
|
||||
}
|
||||
|
||||
// Stringer adds the field key with val.String() (or null if val is nil) to the logger context.
|
||||
func (c Context) Stringer(key string, val fmt.Stringer) Context {
|
||||
if val != nil {
|
||||
c.l.context = enc.AppendString(enc.AppendKey(c.l.context, key), val.String())
|
||||
return c
|
||||
}
|
||||
|
||||
c.l.context = enc.AppendInterface(enc.AppendKey(c.l.context, key), nil)
|
||||
return c
|
||||
}
|
||||
|
||||
// Stringers adds the field key with vals to the logger context where each
|
||||
// individual val is added by calling val.String().
|
||||
//
|
||||
// This is the array version that accepts a slice of fmt.Stringer values.
|
||||
func (c Context) Stringers(key string, vals []fmt.Stringer) Context {
|
||||
c.l.context = enc.AppendStringers(enc.AppendKey(c.l.context, key), vals)
|
||||
return c
|
||||
}
|
||||
|
||||
// StringersV adds the field key with vals to the logger context where each
|
||||
// individual val is added by calling val.String().
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual
|
||||
// fmt.Stringer values.
|
||||
func (c Context) StringersV(key string, vals ...fmt.Stringer) Context {
|
||||
return c.Stringers(key, vals)
|
||||
}
|
||||
|
||||
// Bytes adds the field key with val as a []byte to the logger context.
|
||||
func (c Context) Bytes(key string, val []byte) Context {
|
||||
c.l.context = enc.AppendBytes(enc.AppendKey(c.l.context, key), val)
|
||||
return c
|
||||
}
|
||||
|
||||
// Hex adds the field key with val as a hex string to the logger context.
|
||||
func (c Context) Hex(key string, val []byte) Context {
|
||||
c.l.context = enc.AppendHex(enc.AppendKey(c.l.context, key), val)
|
||||
return c
|
||||
}
|
||||
|
||||
// RawJSON adds already encoded JSON to context.
|
||||
//
|
||||
// No sanity check is performed on b; it must not contain carriage returns and
|
||||
// be valid JSON.
|
||||
func (c Context) RawJSON(key string, b []byte) Context {
|
||||
c.l.context = appendJSON(enc.AppendKey(c.l.context, key), b)
|
||||
return c
|
||||
}
|
||||
|
||||
// AnErr adds the field key with serialized err to the logger context.
|
||||
// If err is nil, no field is added.
|
||||
func (c Context) AnErr(key string, err error) Context {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
return c
|
||||
case LogObjectMarshaler:
|
||||
return c.Object(key, m)
|
||||
case error:
|
||||
if isNilValue(m) {
|
||||
return c
|
||||
}
|
||||
return c.Str(key, m.Error())
|
||||
case string:
|
||||
return c.Str(key, m)
|
||||
default:
|
||||
return c.Interface(key, m)
|
||||
}
|
||||
}
|
||||
|
||||
// Errs adds the field key with errs as an array of serialized errors to the
|
||||
// logger context.
|
||||
func (c Context) Errs(key string, errs []error) Context {
|
||||
arr := c.CreateArray().Errs(errs)
|
||||
return c.Array(key, arr)
|
||||
}
|
||||
|
||||
// Err adds the field "error" with serialized err to the logger context.
|
||||
func (c Context) Err(err error) Context {
|
||||
if c.l.stack && ErrorStackMarshaler != nil {
|
||||
switch m := ErrorStackMarshaler(err).(type) {
|
||||
case nil:
|
||||
return c // do nothing with nil errors
|
||||
case LogObjectMarshaler:
|
||||
c = c.Object(ErrorStackFieldName, m)
|
||||
case error:
|
||||
c = c.Str(ErrorStackFieldName, m.Error())
|
||||
case string:
|
||||
c = c.Str(ErrorStackFieldName, m)
|
||||
default:
|
||||
c = c.Interface(ErrorStackFieldName, m)
|
||||
}
|
||||
}
|
||||
|
||||
return c.AnErr(ErrorFieldName, err)
|
||||
}
|
||||
|
||||
// Ctx adds the context.Context to the logger context. The context.Context is
|
||||
// not rendered in the error message, but is made available for hooks to use.
|
||||
// A typical use case is to extract tracing information from the
|
||||
// context.Context.
|
||||
func (c Context) Ctx(ctx context.Context) Context {
|
||||
c.l.ctx = ctx
|
||||
return c
|
||||
}
|
||||
|
||||
// Bool adds the field key with val as a bool to the logger context.
|
||||
func (c Context) Bool(key string, b bool) Context {
|
||||
c.l.context = enc.AppendBool(enc.AppendKey(c.l.context, key), b)
|
||||
return c
|
||||
}
|
||||
|
||||
// Bools adds the field key with val as a []bool to the logger context.
|
||||
func (c Context) Bools(key string, b []bool) Context {
|
||||
c.l.context = enc.AppendBools(enc.AppendKey(c.l.context, key), b)
|
||||
return c
|
||||
}
|
||||
|
||||
// Int adds the field key with i as a int to the logger context.
|
||||
func (c Context) Int(key string, i int) Context {
|
||||
c.l.context = enc.AppendInt(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Ints adds the field key with i as a []int to the logger context.
|
||||
func (c Context) Ints(key string, i []int) Context {
|
||||
c.l.context = enc.AppendInts(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Int8 adds the field key with i as a int8 to the logger context.
|
||||
func (c Context) Int8(key string, i int8) Context {
|
||||
c.l.context = enc.AppendInt8(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Ints8 adds the field key with i as a []int8 to the logger context.
|
||||
func (c Context) Ints8(key string, i []int8) Context {
|
||||
c.l.context = enc.AppendInts8(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Int16 adds the field key with i as a int16 to the logger context.
|
||||
func (c Context) Int16(key string, i int16) Context {
|
||||
c.l.context = enc.AppendInt16(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Ints16 adds the field key with i as a []int16 to the logger context.
|
||||
func (c Context) Ints16(key string, i []int16) Context {
|
||||
c.l.context = enc.AppendInts16(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Int32 adds the field key with i as a int32 to the logger context.
|
||||
func (c Context) Int32(key string, i int32) Context {
|
||||
c.l.context = enc.AppendInt32(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Ints32 adds the field key with i as a []int32 to the logger context.
|
||||
func (c Context) Ints32(key string, i []int32) Context {
|
||||
c.l.context = enc.AppendInts32(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Int64 adds the field key with i as a int64 to the logger context.
|
||||
func (c Context) Int64(key string, i int64) Context {
|
||||
c.l.context = enc.AppendInt64(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Ints64 adds the field key with i as a []int64 to the logger context.
|
||||
func (c Context) Ints64(key string, i []int64) Context {
|
||||
c.l.context = enc.AppendInts64(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Uint adds the field key with i as a uint to the logger context.
|
||||
func (c Context) Uint(key string, i uint) Context {
|
||||
c.l.context = enc.AppendUint(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Uints adds the field key with i as a []uint to the logger context.
|
||||
func (c Context) Uints(key string, i []uint) Context {
|
||||
c.l.context = enc.AppendUints(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Uint8 adds the field key with i as a uint8 to the logger context.
|
||||
func (c Context) Uint8(key string, i uint8) Context {
|
||||
c.l.context = enc.AppendUint8(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Uints8 adds the field key with i as a []uint8 to the logger context.
|
||||
func (c Context) Uints8(key string, i []uint8) Context {
|
||||
c.l.context = enc.AppendUints8(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Uint16 adds the field key with i as a uint16 to the logger context.
|
||||
func (c Context) Uint16(key string, i uint16) Context {
|
||||
c.l.context = enc.AppendUint16(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Uints16 adds the field key with i as a []uint16 to the logger context.
|
||||
func (c Context) Uints16(key string, i []uint16) Context {
|
||||
c.l.context = enc.AppendUints16(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Uint32 adds the field key with i as a uint32 to the logger context.
|
||||
func (c Context) Uint32(key string, i uint32) Context {
|
||||
c.l.context = enc.AppendUint32(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Uints32 adds the field key with i as a []uint32 to the logger context.
|
||||
func (c Context) Uints32(key string, i []uint32) Context {
|
||||
c.l.context = enc.AppendUints32(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Uint64 adds the field key with i as a uint64 to the logger context.
|
||||
func (c Context) Uint64(key string, i uint64) Context {
|
||||
c.l.context = enc.AppendUint64(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Uints64 adds the field key with i as a []uint64 to the logger context.
|
||||
func (c Context) Uints64(key string, i []uint64) Context {
|
||||
c.l.context = enc.AppendUints64(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Float32 adds the field key with f as a float32 to the logger context.
|
||||
func (c Context) Float32(key string, f float32) Context {
|
||||
c.l.context = enc.AppendFloat32(enc.AppendKey(c.l.context, key), f, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
// Floats32 adds the field key with f as a []float32 to the logger context.
|
||||
func (c Context) Floats32(key string, f []float32) Context {
|
||||
c.l.context = enc.AppendFloats32(enc.AppendKey(c.l.context, key), f, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
// Float64 adds the field key with f as a float64 to the logger context.
|
||||
func (c Context) Float64(key string, f float64) Context {
|
||||
c.l.context = enc.AppendFloat64(enc.AppendKey(c.l.context, key), f, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
// Floats64 adds the field key with f as a []float64 to the logger context.
|
||||
func (c Context) Floats64(key string, f []float64) Context {
|
||||
c.l.context = enc.AppendFloats64(enc.AppendKey(c.l.context, key), f, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
type timestampHook struct{}
|
||||
|
||||
func (ts timestampHook) Run(e *Event, level Level, msg string) {
|
||||
e.Timestamp()
|
||||
}
|
||||
|
||||
var th = timestampHook{}
|
||||
|
||||
// Timestamp adds the current local time to the logger context with the "time" key, formatted using zerolog.TimeFieldFormat.
|
||||
// To customize the key name, change zerolog.TimestampFieldName.
|
||||
// To customize the time format, change zerolog.TimeFieldFormat.
|
||||
//
|
||||
// NOTE: It won't dedupe the "time" key if the *Context has one already.
|
||||
func (c Context) Timestamp() Context {
|
||||
c.l = c.l.Hook(th)
|
||||
return c
|
||||
}
|
||||
|
||||
// Time adds the field key with t formatted as string using zerolog.TimeFieldFormat.
|
||||
func (c Context) Time(key string, t time.Time) Context {
|
||||
c.l.context = enc.AppendTime(enc.AppendKey(c.l.context, key), t, TimeFieldFormat)
|
||||
return c
|
||||
}
|
||||
|
||||
// Times adds the field key with t formatted as string using zerolog.TimeFieldFormat.
|
||||
func (c Context) Times(key string, t []time.Time) Context {
|
||||
c.l.context = enc.AppendTimes(enc.AppendKey(c.l.context, key), t, TimeFieldFormat)
|
||||
return c
|
||||
}
|
||||
|
||||
// Dur adds the field key with d divided by unit and stored as a float.
|
||||
func (c Context) Dur(key string, d time.Duration) Context {
|
||||
c.l.context = enc.AppendDuration(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
// Durs adds the field key with d divided by unit and stored as a float.
|
||||
func (c Context) Durs(key string, d []time.Duration) Context {
|
||||
c.l.context = enc.AppendDurations(enc.AppendKey(c.l.context, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return c
|
||||
}
|
||||
|
||||
// Interface adds the field key with obj marshaled using reflection.
|
||||
func (c Context) Interface(key string, i interface{}) Context {
|
||||
if obj, ok := i.(LogObjectMarshaler); ok {
|
||||
return c.Object(key, obj)
|
||||
}
|
||||
c.l.context = enc.AppendInterface(enc.AppendKey(c.l.context, key), i)
|
||||
return c
|
||||
}
|
||||
|
||||
// Type adds the field key with val's type using reflection.
|
||||
func (c Context) Type(key string, val interface{}) Context {
|
||||
c.l.context = enc.AppendType(enc.AppendKey(c.l.context, key), val)
|
||||
return c
|
||||
}
|
||||
|
||||
// Any is a wrapper around Context.Interface.
|
||||
func (c Context) Any(key string, i interface{}) Context {
|
||||
return c.Interface(key, i)
|
||||
}
|
||||
|
||||
// Reset removes all the context fields.
|
||||
func (c Context) Reset() Context {
|
||||
c.l.context = enc.AppendBeginMarker(make([]byte, 0, 500))
|
||||
return c
|
||||
}
|
||||
|
||||
type callerHook struct {
|
||||
callerSkipFrameCount int
|
||||
}
|
||||
|
||||
func newCallerHook(skipFrameCount int) callerHook {
|
||||
return callerHook{callerSkipFrameCount: skipFrameCount}
|
||||
}
|
||||
|
||||
func (ch callerHook) Run(e *Event, level Level, msg string) {
|
||||
switch ch.callerSkipFrameCount {
|
||||
case useGlobalSkipFrameCount:
|
||||
// Extra frames to skip (added by hook infra).
|
||||
e.caller(CallerSkipFrameCount + contextCallerSkipFrameCount)
|
||||
default:
|
||||
// Extra frames to skip (added by hook infra).
|
||||
e.caller(ch.callerSkipFrameCount + contextCallerSkipFrameCount)
|
||||
}
|
||||
}
|
||||
|
||||
// useGlobalSkipFrameCount acts as a flag to informat callerHook.Run
|
||||
// to use the global CallerSkipFrameCount.
|
||||
const useGlobalSkipFrameCount = math.MinInt32
|
||||
|
||||
// ch is the default caller hook using the global CallerSkipFrameCount.
|
||||
var ch = newCallerHook(useGlobalSkipFrameCount)
|
||||
|
||||
// Caller adds the file:line of the caller with the zerolog.CallerFieldName key.
|
||||
func (c Context) Caller() Context {
|
||||
c.l = c.l.Hook(ch)
|
||||
return c
|
||||
}
|
||||
|
||||
// CallerWithSkipFrameCount adds the file:line of the caller with the zerolog.CallerFieldName key.
|
||||
// The specified skipFrameCount int will override the global CallerSkipFrameCount for this context's respective logger.
|
||||
// If set to -1 the global CallerSkipFrameCount will be used.
|
||||
func (c Context) CallerWithSkipFrameCount(skipFrameCount int) Context {
|
||||
c.l = c.l.Hook(newCallerHook(skipFrameCount))
|
||||
return c
|
||||
}
|
||||
|
||||
// Stack enables stack trace printing for the error passed to Err().
|
||||
func (c Context) Stack() Context {
|
||||
c.l.stack = true
|
||||
return c
|
||||
}
|
||||
|
||||
// IPAddr adds adds the field key with ip as a net.IP IPv4 or IPv6 Address to the context
|
||||
func (c Context) IPAddr(key string, ip net.IP) Context {
|
||||
c.l.context = enc.AppendIPAddr(enc.AppendKey(c.l.context, key), ip)
|
||||
return c
|
||||
}
|
||||
|
||||
// IPAddrs adds the field key with ip as a []net.IP array of IPv4 or IPv6 Address to the context
|
||||
func (c Context) IPAddrs(key string, ip []net.IP) Context {
|
||||
c.l.context = enc.AppendIPAddrs(enc.AppendKey(c.l.context, key), ip)
|
||||
return c
|
||||
}
|
||||
|
||||
// IPPrefix adds adds the field key with pfx as a []net.IPNet IPv4 or IPv6 Prefix (address and mask) to the context
|
||||
func (c Context) IPPrefix(key string, pfx net.IPNet) Context {
|
||||
c.l.context = enc.AppendIPPrefix(enc.AppendKey(c.l.context, key), pfx)
|
||||
return c
|
||||
}
|
||||
|
||||
// IPPrefix adds adds the field key with pfx as a []net.IPNet array of IPv4 or IPv6 Prefix (address and mask) to the context
|
||||
func (c Context) IPPrefixes(key string, pfx []net.IPNet) Context {
|
||||
c.l.context = enc.AppendIPPrefixes(enc.AppendKey(c.l.context, key), pfx)
|
||||
return c
|
||||
}
|
||||
|
||||
// MACAddr adds adds the field key with ha as a net.HardwareAddr MAC address to the context
|
||||
func (c Context) MACAddr(key string, ha net.HardwareAddr) Context {
|
||||
c.l.context = enc.AppendMACAddr(enc.AppendKey(c.l.context, key), ha)
|
||||
return c
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
var disabledLogger *Logger
|
||||
|
||||
func init() {
|
||||
SetGlobalLevel(TraceLevel)
|
||||
l := Nop()
|
||||
disabledLogger = &l
|
||||
}
|
||||
|
||||
type ctxKey struct{}
|
||||
|
||||
// WithContext returns a copy of ctx with the receiver attached. The Logger
|
||||
// attached to the provided Context (if any) will not be effected. If the
|
||||
// receiver's log level is Disabled it will only be attached to the returned
|
||||
// Context if the provided Context has a previously attached Logger. If the
|
||||
// provided Context has no attached Logger, a Disabled Logger will not be
|
||||
// attached.
|
||||
//
|
||||
// Note: to modify the existing Logger attached to a Context (instead of
|
||||
// replacing it in a new Context), use UpdateContext with the following
|
||||
// notation:
|
||||
//
|
||||
// ctx := r.Context()
|
||||
// l := zerolog.Ctx(ctx)
|
||||
// l.UpdateContext(func(c Context) Context {
|
||||
// return c.Str("bar", "baz")
|
||||
// })
|
||||
func (l Logger) WithContext(ctx context.Context) context.Context {
|
||||
if _, ok := ctx.Value(ctxKey{}).(*Logger); !ok && l.level == Disabled {
|
||||
// Do not store disabled logger.
|
||||
return ctx
|
||||
}
|
||||
return context.WithValue(ctx, ctxKey{}, &l)
|
||||
}
|
||||
|
||||
// Ctx returns the Logger associated with the ctx. If no logger
|
||||
// is associated, DefaultContextLogger is returned, unless DefaultContextLogger
|
||||
// is nil, in which case a disabled logger is returned.
|
||||
func Ctx(ctx context.Context) *Logger {
|
||||
if l, ok := ctx.Value(ctxKey{}).(*Logger); ok {
|
||||
return l
|
||||
} else if l = DefaultContextLogger; l != nil {
|
||||
return l
|
||||
}
|
||||
return disabledLogger
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type encoder interface {
|
||||
AppendArrayDelim(dst []byte) []byte
|
||||
AppendArrayEnd(dst []byte) []byte
|
||||
AppendArrayStart(dst []byte) []byte
|
||||
AppendBeginMarker(dst []byte) []byte
|
||||
AppendBool(dst []byte, val bool) []byte
|
||||
AppendBools(dst []byte, vals []bool) []byte
|
||||
AppendBytes(dst, s []byte) []byte
|
||||
AppendDuration(dst []byte, d time.Duration, unit time.Duration, format string, useInt bool, precision int) []byte
|
||||
AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, format string, useInt bool, precision int) []byte
|
||||
AppendEndMarker(dst []byte) []byte
|
||||
AppendFloat32(dst []byte, val float32, precision int) []byte
|
||||
AppendFloat64(dst []byte, val float64, precision int) []byte
|
||||
AppendFloats32(dst []byte, vals []float32, precision int) []byte
|
||||
AppendFloats64(dst []byte, vals []float64, precision int) []byte
|
||||
AppendHex(dst, s []byte) []byte
|
||||
AppendIPAddr(dst []byte, ip net.IP) []byte
|
||||
AppendIPPrefix(dst []byte, pfx net.IPNet) []byte
|
||||
AppendInt(dst []byte, val int) []byte
|
||||
AppendInt16(dst []byte, val int16) []byte
|
||||
AppendInt32(dst []byte, val int32) []byte
|
||||
AppendInt64(dst []byte, val int64) []byte
|
||||
AppendInt8(dst []byte, val int8) []byte
|
||||
AppendInterface(dst []byte, i interface{}) []byte
|
||||
AppendInts(dst []byte, vals []int) []byte
|
||||
AppendInts16(dst []byte, vals []int16) []byte
|
||||
AppendInts32(dst []byte, vals []int32) []byte
|
||||
AppendInts64(dst []byte, vals []int64) []byte
|
||||
AppendInts8(dst []byte, vals []int8) []byte
|
||||
AppendKey(dst []byte, key string) []byte
|
||||
AppendLineBreak(dst []byte) []byte
|
||||
AppendMACAddr(dst []byte, ha net.HardwareAddr) []byte
|
||||
AppendNil(dst []byte) []byte
|
||||
AppendObjectData(dst []byte, o []byte) []byte
|
||||
AppendString(dst []byte, s string) []byte
|
||||
AppendStrings(dst []byte, vals []string) []byte
|
||||
AppendTime(dst []byte, t time.Time, format string) []byte
|
||||
AppendTimes(dst []byte, vals []time.Time, format string) []byte
|
||||
AppendUint(dst []byte, val uint) []byte
|
||||
AppendUint16(dst []byte, val uint16) []byte
|
||||
AppendUint32(dst []byte, val uint32) []byte
|
||||
AppendUint64(dst []byte, val uint64) []byte
|
||||
AppendUint8(dst []byte, val uint8) []byte
|
||||
AppendUints(dst []byte, vals []uint) []byte
|
||||
AppendUints16(dst []byte, vals []uint16) []byte
|
||||
AppendUints32(dst []byte, vals []uint32) []byte
|
||||
AppendUints64(dst []byte, vals []uint64) []byte
|
||||
AppendUints8(dst []byte, vals []uint8) []byte
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
// +build binary_log
|
||||
|
||||
package zerolog
|
||||
|
||||
// This file contains bindings to do binary encoding.
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog/internal/cbor"
|
||||
)
|
||||
|
||||
var (
|
||||
_ encoder = (*cbor.Encoder)(nil)
|
||||
|
||||
enc = cbor.Encoder{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// using closure to reflect the changes at runtime.
|
||||
cbor.JSONMarshalFunc = func(v interface{}) ([]byte, error) {
|
||||
return InterfaceMarshalFunc(v)
|
||||
}
|
||||
}
|
||||
|
||||
func appendJSON(dst []byte, j []byte) []byte {
|
||||
return cbor.AppendEmbeddedJSON(dst, j)
|
||||
}
|
||||
func appendCBOR(dst []byte, c []byte) []byte {
|
||||
return cbor.AppendEmbeddedCBOR(dst, c)
|
||||
}
|
||||
|
||||
// decodeIfBinaryToString - converts a binary formatted log msg to a
|
||||
// JSON formatted String Log message.
|
||||
func decodeIfBinaryToString(in []byte) string {
|
||||
return cbor.DecodeIfBinaryToString(in)
|
||||
}
|
||||
|
||||
func decodeObjectToStr(in []byte) string {
|
||||
return cbor.DecodeObjectToStr(in)
|
||||
}
|
||||
|
||||
// decodeIfBinaryToBytes - converts a binary formatted log msg to a
|
||||
// JSON formatted Bytes Log message.
|
||||
func decodeIfBinaryToBytes(in []byte) []byte {
|
||||
return cbor.DecodeIfBinaryToBytes(in)
|
||||
}
|
||||
+51
@@ -0,0 +1,51 @@
|
||||
// +build !binary_log
|
||||
|
||||
package zerolog
|
||||
|
||||
// encoder_json.go file contains bindings to generate
|
||||
// JSON encoded byte stream.
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"github.com/rs/zerolog/internal/json"
|
||||
)
|
||||
|
||||
var (
|
||||
_ encoder = (*json.Encoder)(nil)
|
||||
|
||||
enc = json.Encoder{}
|
||||
)
|
||||
|
||||
func init() {
|
||||
// using closure to reflect the changes at runtime.
|
||||
json.JSONMarshalFunc = func(v interface{}) ([]byte, error) {
|
||||
return InterfaceMarshalFunc(v)
|
||||
}
|
||||
}
|
||||
|
||||
func appendJSON(dst []byte, j []byte) []byte {
|
||||
return append(dst, j...)
|
||||
}
|
||||
func appendCBOR(dst []byte, cbor []byte) []byte {
|
||||
dst = append(dst, []byte("\"data:application/cbor;base64,")...)
|
||||
l := len(dst)
|
||||
enc := base64.StdEncoding
|
||||
n := enc.EncodedLen(len(cbor))
|
||||
for i := 0; i < n; i++ {
|
||||
dst = append(dst, '.')
|
||||
}
|
||||
enc.Encode(dst[l:], cbor)
|
||||
return append(dst, '"')
|
||||
}
|
||||
|
||||
func decodeIfBinaryToString(in []byte) string {
|
||||
return string(in)
|
||||
}
|
||||
|
||||
func decodeObjectToStr(in []byte) string {
|
||||
return string(in)
|
||||
}
|
||||
|
||||
func decodeIfBinaryToBytes(in []byte) []byte {
|
||||
return in
|
||||
}
|
||||
+918
@@ -0,0 +1,918 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var eventPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return &Event{
|
||||
buf: make([]byte, 0, 500),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Event represents a log event. It is instanced by one of the level method of
|
||||
// Logger and finalized by the Msg or Msgf method.
|
||||
type Event struct {
|
||||
buf []byte
|
||||
w LevelWriter
|
||||
level Level
|
||||
done func(msg string)
|
||||
stack bool // enable error stack trace
|
||||
ch []Hook // hooks from context
|
||||
skipFrame int // The number of additional frames to skip when printing the caller.
|
||||
ctx context.Context // Optional Go context for event
|
||||
}
|
||||
|
||||
func putEvent(e *Event) {
|
||||
// prevent any subsequent use of the Event contextual state and truncate the buffer
|
||||
e.w = nil
|
||||
e.done = nil
|
||||
e.stack = false
|
||||
e.ch = nil
|
||||
e.skipFrame = 0
|
||||
e.ctx = nil
|
||||
e.buf = e.buf[:0]
|
||||
|
||||
// Proper usage of a sync.Pool requires each entry to have approximately
|
||||
// the same memory cost. To obtain this property when the stored type
|
||||
// contains a variably-sized buffer, we add a hard limit on the maximum buffer
|
||||
// to place back in the pool.
|
||||
//
|
||||
// See https://golang.org/issue/23199
|
||||
const maxSize = 1 << 16 // 64KiB
|
||||
if cap(e.buf) <= maxSize {
|
||||
eventPool.Put(e)
|
||||
}
|
||||
}
|
||||
|
||||
// LogObjectMarshaler provides a strongly-typed and encoding-agnostic interface
|
||||
// to be implemented by types used with Event/Context's Object methods.
|
||||
type LogObjectMarshaler interface {
|
||||
MarshalZerologObject(e *Event)
|
||||
}
|
||||
|
||||
// LogArrayMarshaler provides a strongly-typed and encoding-agnostic interface
|
||||
// to be implemented by types used with Event/Context's Array methods.
|
||||
type LogArrayMarshaler interface {
|
||||
MarshalZerologArray(a *Array)
|
||||
}
|
||||
|
||||
func newEvent(w LevelWriter, level Level, stack bool, ctx context.Context, hooks []Hook) *Event {
|
||||
e := eventPool.Get().(*Event)
|
||||
e.buf = e.buf[:0]
|
||||
e.stack = stack
|
||||
e.ctx = ctx
|
||||
e.ch = hooks
|
||||
e.buf = enc.AppendBeginMarker(e.buf)
|
||||
e.w = w
|
||||
e.level = level
|
||||
e.skipFrame = 0
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Event) write() (err error) {
|
||||
if e == nil {
|
||||
return nil
|
||||
}
|
||||
if e.level != Disabled {
|
||||
e.buf = enc.AppendEndMarker(e.buf)
|
||||
e.buf = enc.AppendLineBreak(e.buf)
|
||||
if e.w != nil {
|
||||
_, err = e.w.WriteLevel(e.level, e.buf)
|
||||
}
|
||||
}
|
||||
putEvent(e)
|
||||
return
|
||||
}
|
||||
|
||||
// Enabled return false if the *Event is going to be filtered out by
|
||||
// log level or sampling.
|
||||
func (e *Event) Enabled() bool {
|
||||
return e != nil && e.level != Disabled
|
||||
}
|
||||
|
||||
// Discard disables the event so Msg(f) won't print it.
|
||||
func (e *Event) Discard() *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.level = Disabled
|
||||
return nil
|
||||
}
|
||||
|
||||
// Msg sends the *Event with msg added as the message field if not empty.
|
||||
//
|
||||
// NOTICE: once this method is called, the *Event should be disposed.
|
||||
// Calling Msg twice can have unexpected result.
|
||||
func (e *Event) Msg(msg string) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
e.msg(msg)
|
||||
}
|
||||
|
||||
// Send is equivalent to calling Msg("").
|
||||
//
|
||||
// NOTICE: once this method is called, the *Event should be disposed.
|
||||
func (e *Event) Send() {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
e.msg("")
|
||||
}
|
||||
|
||||
// Msgf sends the event with formatted msg added as the message field if not empty.
|
||||
//
|
||||
// NOTICE: once this method is called, the *Event should be disposed.
|
||||
// Calling Msgf twice can have unexpected result.
|
||||
func (e *Event) Msgf(format string, v ...interface{}) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
e.msg(fmt.Sprintf(format, v...))
|
||||
}
|
||||
|
||||
func (e *Event) MsgFunc(createMsg func() string) {
|
||||
if e == nil {
|
||||
return
|
||||
}
|
||||
e.msg(createMsg())
|
||||
}
|
||||
|
||||
func (e *Event) msg(msg string) {
|
||||
for _, hook := range e.ch {
|
||||
hook.Run(e, e.level, msg)
|
||||
}
|
||||
if msg != "" {
|
||||
e.buf = enc.AppendString(enc.AppendKey(e.buf, MessageFieldName), msg)
|
||||
}
|
||||
if e.done != nil {
|
||||
defer e.done(msg)
|
||||
}
|
||||
if err := e.write(); err != nil {
|
||||
if ErrorHandler != nil {
|
||||
ErrorHandler(err)
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "zerolog: could not write event: %v\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fields is a helper function to use a map or slice to set fields using type assertion.
|
||||
// Only map[string]interface{} and []interface{} are accepted. []interface{} must
|
||||
// alternate string keys and arbitrary values, and extraneous ones are ignored.
|
||||
func (e *Event) Fields(fields interface{}) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = appendFields(e.buf, fields, e.stack, e.ctx, e.ch)
|
||||
return e
|
||||
}
|
||||
|
||||
// Dict adds the field key with a dict to the event context.
|
||||
// Use e.CreateDict() to create the dictionary.
|
||||
func (e *Event) Dict(key string, dict *Event) *Event {
|
||||
if e != nil {
|
||||
dict.buf = enc.AppendEndMarker(dict.buf)
|
||||
e.buf = append(enc.AppendKey(e.buf, key), dict.buf...)
|
||||
}
|
||||
putEvent(dict)
|
||||
return e
|
||||
}
|
||||
|
||||
// CreateDict creates an Event to be used with the *Event.Dict method.
|
||||
// It preserves the stack, hooks, and context from the parent event.
|
||||
// Call usual field methods like Str, Int etc to add fields to this
|
||||
// event and give it as argument the *Event.Dict method.
|
||||
func (e *Event) CreateDict() *Event {
|
||||
if e == nil {
|
||||
return newEvent(nil, DebugLevel, false, nil, nil)
|
||||
}
|
||||
return newEvent(nil, DebugLevel, e.stack, e.ctx, e.ch)
|
||||
}
|
||||
|
||||
// Dict creates an Event to be used with the *Event.Dict method.
|
||||
// Call usual field methods like Str, Int etc to add fields to this
|
||||
// event and give it as argument the *Event.Dict method.
|
||||
// NOTE: This function is deprecated because it does not preserve
|
||||
// the stack, hooks, and context from the parent event.
|
||||
// Deprecated: Use Event.CreateDict instead.
|
||||
func Dict() *Event {
|
||||
return newEvent(nil, DebugLevel, false, nil, nil)
|
||||
}
|
||||
|
||||
// CreateArray creates an Array to be used with the *Event.Array method.
|
||||
// It preserves the stack, hooks, and context from the parent event.
|
||||
// Call usual field methods like Str, Int etc to add elements to this
|
||||
// array and give it as argument the *Event.Array method.
|
||||
func (e *Event) CreateArray() *Array {
|
||||
a := Arr()
|
||||
if e != nil {
|
||||
a.stack = e.stack
|
||||
a.ctx = e.ctx
|
||||
a.ch = e.ch
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
// Array adds the field key with an array to the event context.
|
||||
// Use e.CreateArray() to create the array or pass a type that
|
||||
// implement the LogArrayMarshaler interface.
|
||||
func (e *Event) Array(key string, arr LogArrayMarshaler) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendKey(e.buf, key)
|
||||
var a *Array
|
||||
if aa, ok := arr.(*Array); ok {
|
||||
a = aa
|
||||
} else {
|
||||
a = e.CreateArray()
|
||||
arr.MarshalZerologArray(a)
|
||||
}
|
||||
e.buf = a.write(e.buf)
|
||||
return e
|
||||
}
|
||||
|
||||
func (e *Event) appendObject(obj LogObjectMarshaler) {
|
||||
e.buf = enc.AppendBeginMarker(e.buf)
|
||||
obj.MarshalZerologObject(e)
|
||||
e.buf = enc.AppendEndMarker(e.buf)
|
||||
}
|
||||
|
||||
// Object marshals an object that implement the LogObjectMarshaler interface.
|
||||
func (e *Event) Object(key string, obj LogObjectMarshaler) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendKey(e.buf, key)
|
||||
if obj == nil {
|
||||
e.buf = enc.AppendNil(e.buf)
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
e.appendObject(obj)
|
||||
return e
|
||||
}
|
||||
|
||||
// Objects adds the field key with objs as an array of objects that
|
||||
// implement the LogObjectMarshaler interface to the event.
|
||||
//
|
||||
// This is the array version that accepts a slice of LogObjectMarshaler objects.
|
||||
func (e *Event) Objects(key string, objs []LogObjectMarshaler) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendArrayStart(enc.AppendKey(e.buf, key))
|
||||
for i, obj := range objs {
|
||||
e.buf = appendObject(e.buf, obj, e.stack, e.ctx, e.ch)
|
||||
if i < (len(objs) - 1) {
|
||||
e.buf = enc.AppendArrayDelim(e.buf)
|
||||
}
|
||||
}
|
||||
e.buf = enc.AppendArrayEnd(e.buf)
|
||||
return e
|
||||
}
|
||||
|
||||
// ObjectsV adds the field key with objs as an array of objects that
|
||||
// implement the LogObjectMarshaler interface to the event.
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual LogObjectMarshaler objects.
|
||||
func (e *Event) ObjectsV(key string, objs ...LogObjectMarshaler) *Event {
|
||||
return e.Objects(key, objs)
|
||||
}
|
||||
|
||||
// Func allows an anonymous func to run only if the event is enabled.
|
||||
func (e *Event) Func(f func(e *Event)) *Event {
|
||||
if e != nil && e.Enabled() {
|
||||
f(e)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// EmbedObject marshals an object that implement the LogObjectMarshaler interface.
|
||||
func (e *Event) EmbedObject(obj LogObjectMarshaler) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
if obj == nil {
|
||||
return e
|
||||
}
|
||||
obj.MarshalZerologObject(e)
|
||||
return e
|
||||
}
|
||||
|
||||
// Str adds the field key with val as a string to the *Event context.
|
||||
func (e *Event) Str(key, val string) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendString(enc.AppendKey(e.buf, key), val)
|
||||
return e
|
||||
}
|
||||
|
||||
// Strs adds the field key with vals as a []string to the *Event context.
|
||||
//
|
||||
// This is the array version that accepts a slice of string values.
|
||||
func (e *Event) Strs(key string, vals []string) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendStrings(enc.AppendKey(e.buf, key), vals)
|
||||
return e
|
||||
}
|
||||
|
||||
// StrsV adds the field key with vals as a []string to the *Event context.
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual strings.
|
||||
func (e *Event) StrsV(key string, vals ...string) *Event {
|
||||
return e.Strs(key, vals)
|
||||
}
|
||||
|
||||
// Stringer adds the field key and a val to the *Event context.
|
||||
// If val is not nil, it is added by calling val.String().
|
||||
// If val is nil, it is encoded as null without calling String().
|
||||
func (e *Event) Stringer(key string, val fmt.Stringer) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendStringer(enc.AppendKey(e.buf, key), val)
|
||||
return e
|
||||
}
|
||||
|
||||
// Stringers adds the field key with vals to the *Event context.
|
||||
// If a val is not nil, it is added by calling val.String().
|
||||
// If a val is nil, it is encoded as null without calling String().
|
||||
//
|
||||
// This is the array version that accepts a slice of fmt.Stringer values.
|
||||
func (e *Event) Stringers(key string, vals []fmt.Stringer) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendStringers(enc.AppendKey(e.buf, key), vals)
|
||||
return e
|
||||
}
|
||||
|
||||
// StringersV adds the field key with vals to the *Event context.
|
||||
// If a val is not nil, it is added by calling val.String().
|
||||
// If a val is nil, it is encoded as null without calling String().
|
||||
//
|
||||
// This is a variadic version that accepts a list of individual
|
||||
// fmt.Stringer values.
|
||||
func (e *Event) StringersV(key string, vals ...fmt.Stringer) *Event {
|
||||
return e.Stringers(key, vals)
|
||||
}
|
||||
|
||||
// Bytes adds the field key with val as a string to the *Event context.
|
||||
//
|
||||
// Runes outside of normal ASCII ranges will be hex-encoded in the resulting
|
||||
// JSON.
|
||||
func (e *Event) Bytes(key string, val []byte) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendBytes(enc.AppendKey(e.buf, key), val)
|
||||
return e
|
||||
}
|
||||
|
||||
// Hex adds the field key with val as a hex string to the *Event context.
|
||||
func (e *Event) Hex(key string, val []byte) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendHex(enc.AppendKey(e.buf, key), val)
|
||||
return e
|
||||
}
|
||||
|
||||
// RawJSON adds already encoded JSON to the log line under key.
|
||||
//
|
||||
// No sanity check is performed on b; it must not contain carriage returns and
|
||||
// be valid JSON.
|
||||
func (e *Event) RawJSON(key string, b []byte) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = appendJSON(enc.AppendKey(e.buf, key), b)
|
||||
return e
|
||||
}
|
||||
|
||||
// RawCBOR adds already encoded CBOR to the log line under key.
|
||||
//
|
||||
// No sanity check is performed on b
|
||||
// Note: The full featureset of CBOR is supported as data will not be mapped to json but stored as data-url
|
||||
func (e *Event) RawCBOR(key string, b []byte) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = appendCBOR(enc.AppendKey(e.buf, key), b)
|
||||
return e
|
||||
}
|
||||
|
||||
// AnErr adds the field key with serialized err to the *Event context.
|
||||
// If err is nil, no field is added.
|
||||
func (e *Event) AnErr(key string, err error) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
return e
|
||||
case LogObjectMarshaler:
|
||||
return e.Object(key, m)
|
||||
case error:
|
||||
if isNilValue(m) {
|
||||
return e
|
||||
}
|
||||
return e.Str(key, m.Error())
|
||||
case string:
|
||||
return e.Str(key, m)
|
||||
default:
|
||||
return e.Interface(key, m)
|
||||
}
|
||||
}
|
||||
|
||||
// Errs adds the field key with errs as an array of serialized errors to the
|
||||
// *Event context.
|
||||
func (e *Event) Errs(key string, errs []error) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
arr := e.CreateArray().Errs(errs)
|
||||
return e.Array(key, arr)
|
||||
}
|
||||
|
||||
// Err adds the field "error" with serialized err to the *Event context.
|
||||
// If err is nil, no field is added.
|
||||
//
|
||||
// To customize the key name, change zerolog.ErrorFieldName.
|
||||
//
|
||||
// If Stack() has been called before and zerolog.ErrorStackMarshaler is defined,
|
||||
// the err is passed to ErrorStackMarshaler and the result is appended to the
|
||||
// zerolog.ErrorStackFieldName.
|
||||
func (e *Event) Err(err error) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
|
||||
if e.stack && ErrorStackMarshaler != nil {
|
||||
switch m := ErrorStackMarshaler(err).(type) {
|
||||
case nil:
|
||||
return e
|
||||
case LogObjectMarshaler:
|
||||
e = e.Object(ErrorStackFieldName, m)
|
||||
case error:
|
||||
e = e.Str(ErrorStackFieldName, m.Error())
|
||||
case string:
|
||||
e = e.Str(ErrorStackFieldName, m)
|
||||
default:
|
||||
e = e.Interface(ErrorStackFieldName, m)
|
||||
}
|
||||
}
|
||||
|
||||
return e.AnErr(ErrorFieldName, err)
|
||||
}
|
||||
|
||||
// Stack enables stack trace printing for the error passed to Err().
|
||||
//
|
||||
// ErrorStackMarshaler must be set for this method to do something.
|
||||
func (e *Event) Stack() *Event {
|
||||
if e != nil {
|
||||
e.stack = true
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// Ctx adds the Go Context to the *Event context. The context is not rendered
|
||||
// in the output message, but is available to hooks and to Func() calls via the
|
||||
// GetCtx() accessor. A typical use case is to extract tracing information from
|
||||
// the Go Ctx.
|
||||
func (e *Event) Ctx(ctx context.Context) *Event {
|
||||
if e != nil {
|
||||
e.ctx = ctx
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// GetCtx retrieves the Go context.Context which is optionally stored in the
|
||||
// Event. This allows Hooks and functions passed to Func() to retrieve values
|
||||
// which are stored in the context.Context. This can be useful in tracing,
|
||||
// where span information is commonly propagated in the context.Context.
|
||||
func (e *Event) GetCtx() context.Context {
|
||||
if e == nil || e.ctx == nil {
|
||||
return context.Background()
|
||||
}
|
||||
return e.ctx
|
||||
}
|
||||
|
||||
// Bool adds the field key with val as a bool to the *Event context.
|
||||
func (e *Event) Bool(key string, b bool) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendBool(enc.AppendKey(e.buf, key), b)
|
||||
return e
|
||||
}
|
||||
|
||||
// Bools adds the field key with val as a []bool to the *Event context.
|
||||
func (e *Event) Bools(key string, b []bool) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendBools(enc.AppendKey(e.buf, key), b)
|
||||
return e
|
||||
}
|
||||
|
||||
// Int adds the field key with i as a int to the *Event context.
|
||||
func (e *Event) Int(key string, i int) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendInt(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Ints adds the field key with i as a []int to the *Event context.
|
||||
func (e *Event) Ints(key string, i []int) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendInts(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Int8 adds the field key with i as a int8 to the *Event context.
|
||||
func (e *Event) Int8(key string, i int8) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendInt8(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Ints8 adds the field key with i as a []int8 to the *Event context.
|
||||
func (e *Event) Ints8(key string, i []int8) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendInts8(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Int16 adds the field key with i as a int16 to the *Event context.
|
||||
func (e *Event) Int16(key string, i int16) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendInt16(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Ints16 adds the field key with i as a []int16 to the *Event context.
|
||||
func (e *Event) Ints16(key string, i []int16) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendInts16(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Int32 adds the field key with i as a int32 to the *Event context.
|
||||
func (e *Event) Int32(key string, i int32) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendInt32(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Ints32 adds the field key with i as a []int32 to the *Event context.
|
||||
func (e *Event) Ints32(key string, i []int32) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendInts32(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Int64 adds the field key with i as a int64 to the *Event context.
|
||||
func (e *Event) Int64(key string, i int64) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendInt64(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Ints64 adds the field key with i as a []int64 to the *Event context.
|
||||
func (e *Event) Ints64(key string, i []int64) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendInts64(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Uint adds the field key with i as a uint to the *Event context.
|
||||
func (e *Event) Uint(key string, i uint) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendUint(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Uints adds the field key with i as a []int to the *Event context.
|
||||
func (e *Event) Uints(key string, i []uint) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendUints(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Uint8 adds the field key with i as a uint8 to the *Event context.
|
||||
func (e *Event) Uint8(key string, i uint8) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendUint8(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Uints8 adds the field key with i as a []int8 to the *Event context.
|
||||
func (e *Event) Uints8(key string, i []uint8) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendUints8(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Uint16 adds the field key with i as a uint16 to the *Event context.
|
||||
func (e *Event) Uint16(key string, i uint16) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendUint16(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Uints16 adds the field key with i as a []int16 to the *Event context.
|
||||
func (e *Event) Uints16(key string, i []uint16) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendUints16(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Uint32 adds the field key with i as a uint32 to the *Event context.
|
||||
func (e *Event) Uint32(key string, i uint32) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendUint32(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Uints32 adds the field key with i as a []int32 to the *Event context.
|
||||
func (e *Event) Uints32(key string, i []uint32) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendUints32(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Uint64 adds the field key with i as a uint64 to the *Event context.
|
||||
func (e *Event) Uint64(key string, i uint64) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendUint64(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Uints64 adds the field key with i as a []int64 to the *Event context.
|
||||
func (e *Event) Uints64(key string, i []uint64) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendUints64(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Float32 adds the field key with f as a float32 to the *Event context.
|
||||
func (e *Event) Float32(key string, f float32) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendFloat32(enc.AppendKey(e.buf, key), f, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
// Floats32 adds the field key with f as a []float32 to the *Event context.
|
||||
func (e *Event) Floats32(key string, f []float32) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendFloats32(enc.AppendKey(e.buf, key), f, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
// Float64 adds the field key with f as a float64 to the *Event context.
|
||||
func (e *Event) Float64(key string, f float64) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendFloat64(enc.AppendKey(e.buf, key), f, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
// Floats64 adds the field key with f as a []float64 to the *Event context.
|
||||
func (e *Event) Floats64(key string, f []float64) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendFloats64(enc.AppendKey(e.buf, key), f, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
// Timestamp adds the current local time as UNIX timestamp to the *Event context with the "time" key.
|
||||
// To customize the key name, change zerolog.TimestampFieldName.
|
||||
//
|
||||
// NOTE: It won't dedupe the "time" key if the *Event (or *Context) has one
|
||||
// already.
|
||||
func (e *Event) Timestamp() *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendTime(enc.AppendKey(e.buf, TimestampFieldName), TimestampFunc(), TimeFieldFormat)
|
||||
return e
|
||||
}
|
||||
|
||||
// Time adds the field key with t formatted as string using zerolog.TimeFieldFormat.
|
||||
func (e *Event) Time(key string, t time.Time) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendTime(enc.AppendKey(e.buf, key), t, TimeFieldFormat)
|
||||
return e
|
||||
}
|
||||
|
||||
// Times adds the field key with t formatted as string using zerolog.TimeFieldFormat.
|
||||
func (e *Event) Times(key string, t []time.Time) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendTimes(enc.AppendKey(e.buf, key), t, TimeFieldFormat)
|
||||
return e
|
||||
}
|
||||
|
||||
// Dur adds the field key with duration d stored as zerolog.DurationFieldUnit.
|
||||
// If zerolog.DurationFieldInteger is true, durations are rendered as integer
|
||||
// instead of float.
|
||||
func (e *Event) Dur(key string, d time.Duration) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
// Durs adds the field key with duration d stored as zerolog.DurationFieldUnit.
|
||||
// If zerolog.DurationFieldInteger is true, durations are rendered as integer
|
||||
// instead of float.
|
||||
func (e *Event) Durs(key string, d []time.Duration) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendDurations(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
// TimeDiff adds the field key with positive duration between time t and start.
|
||||
// If time t is not greater than start, duration will be 0.
|
||||
// Duration format follows the same principle as Dur().
|
||||
func (e *Event) TimeDiff(key string, t time.Time, start time.Time) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
var d time.Duration
|
||||
if t.After(start) {
|
||||
d = t.Sub(start)
|
||||
}
|
||||
e.buf = enc.AppendDuration(enc.AppendKey(e.buf, key), d, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
return e
|
||||
}
|
||||
|
||||
// Any is a wrapper around Event.Interface.
|
||||
func (e *Event) Any(key string, i interface{}) *Event {
|
||||
return e.Interface(key, i)
|
||||
}
|
||||
|
||||
// Interface adds the field key with i marshaled using reflection.
|
||||
func (e *Event) Interface(key string, i interface{}) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
if obj, ok := i.(LogObjectMarshaler); ok {
|
||||
return e.Object(key, obj)
|
||||
}
|
||||
e.buf = enc.AppendInterface(enc.AppendKey(e.buf, key), i)
|
||||
return e
|
||||
}
|
||||
|
||||
// Type adds the field key with val's type using reflection.
|
||||
func (e *Event) Type(key string, val interface{}) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendType(enc.AppendKey(e.buf, key), val)
|
||||
return e
|
||||
}
|
||||
|
||||
// CallerSkipFrame instructs any future Caller calls to skip the specified number of frames.
|
||||
// This includes those added via hooks from the context.
|
||||
func (e *Event) CallerSkipFrame(skip int) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.skipFrame += skip
|
||||
return e
|
||||
}
|
||||
|
||||
// Caller adds the file:line of the caller with the zerolog.CallerFieldName key.
|
||||
// The argument skip is the number of stack frames to ascend
|
||||
// Skip If not passed, use the global variable CallerSkipFrameCount
|
||||
func (e *Event) Caller(skip ...int) *Event {
|
||||
sk := CallerSkipFrameCount
|
||||
if len(skip) > 0 {
|
||||
sk = skip[0] + CallerSkipFrameCount
|
||||
}
|
||||
return e.caller(sk)
|
||||
}
|
||||
|
||||
func (e *Event) caller(skip int) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
if pc, file, line, ok := runtime.Caller(skip + e.skipFrame); ok {
|
||||
e.buf = enc.AppendString(enc.AppendKey(e.buf, CallerFieldName), CallerMarshalFunc(pc, file, line))
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
// IPAddr adds the field key with ip as a net.IP IPv4 or IPv6 Address to the event
|
||||
func (e *Event) IPAddr(key string, ip net.IP) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendIPAddr(enc.AppendKey(e.buf, key), ip)
|
||||
return e
|
||||
}
|
||||
|
||||
// IPAddrs adds the field key with ip as a net.IP array of IPv4 or IPv6 Address to the event
|
||||
func (e *Event) IPAddrs(key string, ip []net.IP) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendIPAddrs(enc.AppendKey(e.buf, key), ip)
|
||||
return e
|
||||
}
|
||||
|
||||
// IPPrefix adds the field key with pfx as a net.IPNet IPv4 or IPv6 Prefix (address and mask) to the event
|
||||
func (e *Event) IPPrefix(key string, pfx net.IPNet) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendIPPrefix(enc.AppendKey(e.buf, key), pfx)
|
||||
return e
|
||||
}
|
||||
|
||||
// IPPrefixes the field key with pfx as a net.IPNet array of IPv4 or IPv6 Prefixes (address and mask) to the event
|
||||
func (e *Event) IPPrefixes(key string, pfx []net.IPNet) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendIPPrefixes(enc.AppendKey(e.buf, key), pfx)
|
||||
return e
|
||||
}
|
||||
|
||||
// MACAddr the field key with ha as a net.HardwareAddr MAC address to the event
|
||||
func (e *Event) MACAddr(key string, ha net.HardwareAddr) *Event {
|
||||
if e == nil {
|
||||
return e
|
||||
}
|
||||
e.buf = enc.AppendMACAddr(enc.AppendKey(e.buf, key), ha)
|
||||
return e
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
{"time":"5:41PM","level":"info","message":"Starting listener","listen":":8080","pid":37556}
|
||||
{"time":"5:41PM","level":"debug","message":"Access","database":"myapp","host":"localhost:4962","pid":37556}
|
||||
{"time":"5:41PM","level":"info","message":"Access","method":"GET","path":"/users","pid":37556,"resp_time":23}
|
||||
{"time":"5:41PM","level":"info","message":"Access","method":"POST","path":"/posts","pid":37556,"resp_time":532}
|
||||
{"time":"5:41PM","level":"warn","message":"Slow request","method":"POST","path":"/posts","pid":37556,"resp_time":532}
|
||||
{"time":"5:41PM","level":"info","message":"Access","method":"GET","path":"/users","pid":37556,"resp_time":10}
|
||||
{"time":"5:41PM","level":"error","message":"Database connection lost","database":"myapp","pid":37556,"error":"connection reset by peer"}
|
||||
+312
@@ -0,0 +1,312 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
func isNilValue(e error) bool {
|
||||
switch reflect.TypeOf(e).Kind() {
|
||||
case reflect.Ptr:
|
||||
return reflect.ValueOf(e).IsNil()
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func appendFields(dst []byte, fields interface{}, stack bool, ctx context.Context, hooks []Hook) []byte {
|
||||
switch fields := fields.(type) {
|
||||
case []interface{}:
|
||||
if n := len(fields); n&0x1 == 1 { // odd number
|
||||
fields = fields[:n-1]
|
||||
}
|
||||
dst = appendFieldList(dst, fields, stack, ctx, hooks)
|
||||
case map[string]interface{}:
|
||||
keys := make([]string, 0, len(fields))
|
||||
for key := range fields {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
kv := make([]interface{}, 2)
|
||||
for _, key := range keys {
|
||||
kv[0], kv[1] = key, fields[key]
|
||||
dst = appendFieldList(dst, kv, stack, ctx, hooks)
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendObject(dst []byte, obj LogObjectMarshaler, stack bool, ctx context.Context, hooks []Hook) []byte {
|
||||
e := newEvent(LevelWriterAdapter{io.Discard}, DebugLevel, stack, ctx, hooks)
|
||||
e.buf = e.buf[:0] // discard the beginning marker added by newEvent
|
||||
e.appendObject(obj)
|
||||
dst = append(dst, e.buf...)
|
||||
putEvent(e)
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendFieldList(dst []byte, kvList []interface{}, stack bool, ctx context.Context, hooks []Hook) []byte {
|
||||
for i, n := 0, len(kvList); i < n; i += 2 {
|
||||
key, val := kvList[i], kvList[i+1]
|
||||
if key, ok := key.(string); ok {
|
||||
dst = enc.AppendKey(dst, key)
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
switch val := val.(type) {
|
||||
case string:
|
||||
dst = enc.AppendString(dst, val)
|
||||
case []byte:
|
||||
dst = enc.AppendBytes(dst, val)
|
||||
case error:
|
||||
switch m := ErrorMarshalFunc(val).(type) {
|
||||
case nil:
|
||||
dst = enc.AppendNil(dst)
|
||||
case LogObjectMarshaler:
|
||||
dst = appendObject(dst, m, stack, ctx, hooks)
|
||||
case error:
|
||||
if !isNilValue(m) {
|
||||
dst = enc.AppendString(dst, m.Error())
|
||||
}
|
||||
case string:
|
||||
dst = enc.AppendString(dst, m)
|
||||
default:
|
||||
dst = enc.AppendInterface(dst, m)
|
||||
}
|
||||
|
||||
if stack && ErrorStackMarshaler != nil {
|
||||
switch m := ErrorStackMarshaler(val).(type) {
|
||||
case nil:
|
||||
return dst // do nothing with nil errors
|
||||
case LogObjectMarshaler:
|
||||
dst = enc.AppendKey(dst, ErrorStackFieldName)
|
||||
dst = appendObject(dst, m, stack, ctx, hooks)
|
||||
case error:
|
||||
dst = enc.AppendKey(dst, ErrorStackFieldName)
|
||||
dst = enc.AppendString(dst, m.Error())
|
||||
case string:
|
||||
dst = enc.AppendKey(dst, ErrorStackFieldName)
|
||||
dst = enc.AppendString(dst, m)
|
||||
default:
|
||||
dst = enc.AppendKey(dst, ErrorStackFieldName)
|
||||
dst = enc.AppendInterface(dst, m)
|
||||
}
|
||||
}
|
||||
case []error:
|
||||
dst = enc.AppendArrayStart(dst)
|
||||
for i, err := range val {
|
||||
switch m := ErrorMarshalFunc(err).(type) {
|
||||
case nil:
|
||||
dst = enc.AppendNil(dst)
|
||||
case LogObjectMarshaler:
|
||||
dst = appendObject(dst, m, stack, ctx, hooks)
|
||||
case error:
|
||||
if !isNilValue(m) {
|
||||
dst = enc.AppendString(dst, m.Error())
|
||||
}
|
||||
case string:
|
||||
dst = enc.AppendString(dst, m)
|
||||
default:
|
||||
dst = enc.AppendInterface(dst, m)
|
||||
}
|
||||
|
||||
if i < (len(val) - 1) {
|
||||
dst = enc.AppendArrayDelim(dst)
|
||||
}
|
||||
}
|
||||
dst = enc.AppendArrayEnd(dst)
|
||||
case []LogObjectMarshaler:
|
||||
dst = enc.AppendArrayStart(dst)
|
||||
for i, obj := range val {
|
||||
dst = appendObject(dst, obj, stack, ctx, hooks)
|
||||
if i < (len(val) - 1) {
|
||||
dst = enc.AppendArrayDelim(dst)
|
||||
}
|
||||
}
|
||||
dst = enc.AppendArrayEnd(dst)
|
||||
case bool:
|
||||
dst = enc.AppendBool(dst, val)
|
||||
case int:
|
||||
dst = enc.AppendInt(dst, val)
|
||||
case int8:
|
||||
dst = enc.AppendInt8(dst, val)
|
||||
case int16:
|
||||
dst = enc.AppendInt16(dst, val)
|
||||
case int32:
|
||||
dst = enc.AppendInt32(dst, val)
|
||||
case int64:
|
||||
dst = enc.AppendInt64(dst, val)
|
||||
case uint:
|
||||
dst = enc.AppendUint(dst, val)
|
||||
case uint8:
|
||||
dst = enc.AppendUint8(dst, val)
|
||||
case uint16:
|
||||
dst = enc.AppendUint16(dst, val)
|
||||
case uint32:
|
||||
dst = enc.AppendUint32(dst, val)
|
||||
case uint64:
|
||||
dst = enc.AppendUint64(dst, val)
|
||||
case float32:
|
||||
dst = enc.AppendFloat32(dst, val, FloatingPointPrecision)
|
||||
case float64:
|
||||
dst = enc.AppendFloat64(dst, val, FloatingPointPrecision)
|
||||
case time.Time:
|
||||
dst = enc.AppendTime(dst, val, TimeFieldFormat)
|
||||
case time.Duration:
|
||||
dst = enc.AppendDuration(dst, val, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
case *string:
|
||||
if val != nil {
|
||||
dst = enc.AppendString(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *bool:
|
||||
if val != nil {
|
||||
dst = enc.AppendBool(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *int:
|
||||
if val != nil {
|
||||
dst = enc.AppendInt(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *int8:
|
||||
if val != nil {
|
||||
dst = enc.AppendInt8(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *int16:
|
||||
if val != nil {
|
||||
dst = enc.AppendInt16(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *int32:
|
||||
if val != nil {
|
||||
dst = enc.AppendInt32(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *int64:
|
||||
if val != nil {
|
||||
dst = enc.AppendInt64(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *uint:
|
||||
if val != nil {
|
||||
dst = enc.AppendUint(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *uint8:
|
||||
if val != nil {
|
||||
dst = enc.AppendUint8(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *uint16:
|
||||
if val != nil {
|
||||
dst = enc.AppendUint16(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *uint32:
|
||||
if val != nil {
|
||||
dst = enc.AppendUint32(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *uint64:
|
||||
if val != nil {
|
||||
dst = enc.AppendUint64(dst, *val)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *float32:
|
||||
if val != nil {
|
||||
dst = enc.AppendFloat32(dst, *val, FloatingPointPrecision)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *float64:
|
||||
if val != nil {
|
||||
dst = enc.AppendFloat64(dst, *val, FloatingPointPrecision)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *time.Time:
|
||||
if val != nil {
|
||||
dst = enc.AppendTime(dst, *val, TimeFieldFormat)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case *time.Duration:
|
||||
if val != nil {
|
||||
dst = enc.AppendDuration(dst, *val, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
} else {
|
||||
dst = enc.AppendNil(dst)
|
||||
}
|
||||
case []string:
|
||||
dst = enc.AppendStrings(dst, val)
|
||||
case []bool:
|
||||
dst = enc.AppendBools(dst, val)
|
||||
case []int:
|
||||
dst = enc.AppendInts(dst, val)
|
||||
case []int8:
|
||||
dst = enc.AppendInts8(dst, val)
|
||||
case []int16:
|
||||
dst = enc.AppendInts16(dst, val)
|
||||
case []int32:
|
||||
dst = enc.AppendInts32(dst, val)
|
||||
case []int64:
|
||||
dst = enc.AppendInts64(dst, val)
|
||||
case []uint:
|
||||
dst = enc.AppendUints(dst, val)
|
||||
// case []uint8: is handled as []byte above
|
||||
case []uint16:
|
||||
dst = enc.AppendUints16(dst, val)
|
||||
case []uint32:
|
||||
dst = enc.AppendUints32(dst, val)
|
||||
case []uint64:
|
||||
dst = enc.AppendUints64(dst, val)
|
||||
case []float32:
|
||||
dst = enc.AppendFloats32(dst, val, FloatingPointPrecision)
|
||||
case []float64:
|
||||
dst = enc.AppendFloats64(dst, val, FloatingPointPrecision)
|
||||
case []time.Time:
|
||||
dst = enc.AppendTimes(dst, val, TimeFieldFormat)
|
||||
case []time.Duration:
|
||||
dst = enc.AppendDurations(dst, val, DurationFieldUnit, DurationFieldFormat, DurationFieldInteger, FloatingPointPrecision)
|
||||
case nil:
|
||||
dst = enc.AppendNil(dst)
|
||||
case net.IP:
|
||||
dst = enc.AppendIPAddr(dst, val)
|
||||
case []net.IP:
|
||||
dst = enc.AppendIPAddrs(dst, val)
|
||||
case net.IPNet:
|
||||
dst = enc.AppendIPPrefix(dst, val)
|
||||
case []net.IPNet:
|
||||
dst = enc.AppendIPPrefixes(dst, val)
|
||||
case net.HardwareAddr:
|
||||
dst = enc.AppendMACAddr(dst, val)
|
||||
case json.RawMessage:
|
||||
dst = appendJSON(dst, val)
|
||||
default:
|
||||
if lom, ok := val.(LogObjectMarshaler); ok {
|
||||
dst = appendObject(dst, lom, stack, ctx, hooks)
|
||||
} else {
|
||||
dst = enc.AppendInterface(dst, val)
|
||||
}
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
+208
@@ -0,0 +1,208 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// TimeFormatUnix defines a time format that makes time fields to be
|
||||
// serialized as Unix timestamp integers.
|
||||
TimeFormatUnix = ""
|
||||
|
||||
// TimeFormatUnixMs defines a time format that makes time fields to be
|
||||
// serialized as Unix timestamp integers in milliseconds.
|
||||
TimeFormatUnixMs = "UNIXMS"
|
||||
|
||||
// TimeFormatUnixMicro defines a time format that makes time fields to be
|
||||
// serialized as Unix timestamp integers in microseconds.
|
||||
TimeFormatUnixMicro = "UNIXMICRO"
|
||||
|
||||
// TimeFormatUnixNano defines a time format that makes time fields to be
|
||||
// serialized as Unix timestamp integers in nanoseconds.
|
||||
TimeFormatUnixNano = "UNIXNANO"
|
||||
|
||||
// DurationFormatFloat defines a format for Duration fields that makes duration fields to be
|
||||
// serialized as floating point numbers.
|
||||
DurationFormatFloat = "float"
|
||||
// DurationFormatInt defines a format for Duration fields that makes duration fields to be
|
||||
// serialized as integers.
|
||||
DurationFormatInt = "int"
|
||||
// DurationFormatString defines a format for Duration fields that makes duration fields to be
|
||||
// serialized as string.
|
||||
DurationFormatString = "string"
|
||||
)
|
||||
|
||||
var (
|
||||
// TimestampFieldName is the field name used for the timestamp field.
|
||||
TimestampFieldName = "time"
|
||||
|
||||
// LevelFieldName is the field name used for the level field.
|
||||
LevelFieldName = "level"
|
||||
|
||||
// LevelTraceValue is the value used for the trace level field.
|
||||
LevelTraceValue = "trace"
|
||||
// LevelDebugValue is the value used for the debug level field.
|
||||
LevelDebugValue = "debug"
|
||||
// LevelInfoValue is the value used for the info level field.
|
||||
LevelInfoValue = "info"
|
||||
// LevelWarnValue is the value used for the warn level field.
|
||||
LevelWarnValue = "warn"
|
||||
// LevelErrorValue is the value used for the error level field.
|
||||
LevelErrorValue = "error"
|
||||
// LevelFatalValue is the value used for the fatal level field.
|
||||
LevelFatalValue = "fatal"
|
||||
// LevelPanicValue is the value used for the panic level field.
|
||||
LevelPanicValue = "panic"
|
||||
|
||||
// LevelFieldMarshalFunc allows customization of global level field marshaling.
|
||||
LevelFieldMarshalFunc = func(l Level) string {
|
||||
return l.String()
|
||||
}
|
||||
|
||||
// MessageFieldName is the field name used for the message field.
|
||||
MessageFieldName = "message"
|
||||
|
||||
// ErrorFieldName is the field name used for error fields.
|
||||
ErrorFieldName = "error"
|
||||
|
||||
// CallerFieldName is the field name used for caller field.
|
||||
CallerFieldName = "caller"
|
||||
|
||||
// CallerSkipFrameCount is the number of stack frames to skip to find the caller.
|
||||
CallerSkipFrameCount = 2
|
||||
|
||||
// CallerMarshalFunc allows customization of global caller marshaling
|
||||
CallerMarshalFunc = func(pc uintptr, file string, line int) string {
|
||||
return file + ":" + strconv.Itoa(line)
|
||||
}
|
||||
|
||||
// ErrorStackFieldName is the field name used for error stacks.
|
||||
ErrorStackFieldName = "stack"
|
||||
|
||||
// ErrorStackMarshaler extract the stack from err if any.
|
||||
ErrorStackMarshaler func(err error) interface{}
|
||||
|
||||
// ErrorMarshalFunc allows customization of global error marshaling
|
||||
ErrorMarshalFunc = func(err error) interface{} {
|
||||
return err
|
||||
}
|
||||
|
||||
// InterfaceMarshalFunc allows customization of interface marshaling.
|
||||
// Default: "encoding/json.Marshal" with disabled HTML escaping
|
||||
InterfaceMarshalFunc = func(v interface{}) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
encoder := json.NewEncoder(&buf)
|
||||
encoder.SetEscapeHTML(false)
|
||||
err := encoder.Encode(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
b := buf.Bytes()
|
||||
if len(b) > 0 {
|
||||
// Remove trailing \n which is added by Encode.
|
||||
return b[:len(b)-1], nil
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
// TimeFieldFormat defines the time format of the Time field type. If set to
|
||||
// TimeFormatUnix, TimeFormatUnixMs, TimeFormatUnixMicro or TimeFormatUnixNano, the time is formatted as a UNIX
|
||||
// timestamp as integer.
|
||||
TimeFieldFormat = time.RFC3339
|
||||
|
||||
// TimestampFunc defines the function called to generate a timestamp.
|
||||
TimestampFunc = time.Now
|
||||
|
||||
// DurationFieldFormat defines the format of the Duration field type.
|
||||
DurationFieldFormat = DurationFormatFloat
|
||||
|
||||
// DurationFieldUnit defines the unit for time.Duration type fields added
|
||||
// using the Dur method.
|
||||
DurationFieldUnit = time.Millisecond
|
||||
|
||||
// DurationFieldInteger renders Dur fields as integer instead of float if
|
||||
// set to true.
|
||||
// Deprecated: use DurationFieldFormat with DurationFormatInt instead.
|
||||
DurationFieldInteger = false
|
||||
|
||||
// ErrorHandler is called whenever zerolog fails to write an event on its
|
||||
// output. If not set, an error is printed on the stderr. This handler must
|
||||
// be thread safe and non-blocking.
|
||||
ErrorHandler func(err error)
|
||||
|
||||
// FatalExitFunc is called by log.Fatal() instead of os.Exit(1). If not set,
|
||||
// os.Exit(1) is called.
|
||||
FatalExitFunc func()
|
||||
|
||||
// DefaultContextLogger is returned from Ctx() if there is no logger associated
|
||||
// with the context.
|
||||
DefaultContextLogger *Logger
|
||||
|
||||
// LevelColors are used by ConsoleWriter's consoleDefaultFormatLevel to color
|
||||
// log levels.
|
||||
LevelColors = map[Level]int{
|
||||
TraceLevel: colorBlue,
|
||||
DebugLevel: 0,
|
||||
InfoLevel: colorGreen,
|
||||
WarnLevel: colorYellow,
|
||||
ErrorLevel: colorRed,
|
||||
FatalLevel: colorRed,
|
||||
PanicLevel: colorRed,
|
||||
}
|
||||
|
||||
// FormattedLevels are used by ConsoleWriter's consoleDefaultFormatLevel
|
||||
// for a short level name.
|
||||
FormattedLevels = map[Level]string{
|
||||
TraceLevel: "TRC",
|
||||
DebugLevel: "DBG",
|
||||
InfoLevel: "INF",
|
||||
WarnLevel: "WRN",
|
||||
ErrorLevel: "ERR",
|
||||
FatalLevel: "FTL",
|
||||
PanicLevel: "PNC",
|
||||
}
|
||||
|
||||
// TriggerLevelWriterBufferReuseLimit is a limit in bytes that a buffer is dropped
|
||||
// from the TriggerLevelWriter buffer pool if the buffer grows above the limit.
|
||||
TriggerLevelWriterBufferReuseLimit = 64 * 1024
|
||||
|
||||
// FloatingPointPrecision, if set to a value other than -1, controls the number
|
||||
// of digits when formatting float numbers in JSON. See strconv.FormatFloat for
|
||||
// more details.
|
||||
FloatingPointPrecision = -1
|
||||
)
|
||||
|
||||
var (
|
||||
gLevel = new(int32)
|
||||
disableSampling = new(int32)
|
||||
)
|
||||
|
||||
// SetGlobalLevel sets the global override for log level. If this
|
||||
// values is raised, all Loggers will use at least this value.
|
||||
//
|
||||
// To globally disable logs, set GlobalLevel to Disabled.
|
||||
func SetGlobalLevel(l Level) {
|
||||
atomic.StoreInt32(gLevel, int32(l))
|
||||
}
|
||||
|
||||
// GlobalLevel returns the current global log level
|
||||
func GlobalLevel() Level {
|
||||
return Level(atomic.LoadInt32(gLevel))
|
||||
}
|
||||
|
||||
// DisableSampling will disable sampling in all Loggers if true.
|
||||
func DisableSampling(v bool) {
|
||||
var i int32
|
||||
if v {
|
||||
i = 1
|
||||
}
|
||||
atomic.StoreInt32(disableSampling, i)
|
||||
}
|
||||
|
||||
func samplingDisabled() bool {
|
||||
return atomic.LoadInt32(disableSampling) == 1
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
//go:build go1.18
|
||||
// +build go1.18
|
||||
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func AsLogObjectMarshalers[T LogObjectMarshaler](objs []T) []LogObjectMarshaler {
|
||||
if objs == nil {
|
||||
return nil
|
||||
}
|
||||
s := make([]LogObjectMarshaler, len(objs))
|
||||
for i, v := range objs {
|
||||
s[i] = v
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func AsStringers[T fmt.Stringer](objs []T) []fmt.Stringer {
|
||||
if objs == nil {
|
||||
return nil
|
||||
}
|
||||
s := make([]fmt.Stringer, len(objs))
|
||||
for i, v := range objs {
|
||||
s[i] = v
|
||||
}
|
||||
return s
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
// +build go1.12
|
||||
|
||||
package zerolog
|
||||
|
||||
// Since go 1.12, some auto generated init functions are hidden from
|
||||
// runtime.Caller.
|
||||
const contextCallerSkipFrameCount = 2
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
package zerolog
|
||||
|
||||
// Hook defines an interface to a log hook.
|
||||
type Hook interface {
|
||||
// Run runs the hook with the event.
|
||||
Run(e *Event, level Level, message string)
|
||||
}
|
||||
|
||||
// HookFunc is an adaptor to allow the use of an ordinary function
|
||||
// as a Hook.
|
||||
type HookFunc func(e *Event, level Level, message string)
|
||||
|
||||
// Run implements the Hook interface.
|
||||
func (h HookFunc) Run(e *Event, level Level, message string) {
|
||||
h(e, level, message)
|
||||
}
|
||||
|
||||
// LevelHook applies a different hook for each level.
|
||||
type LevelHook struct {
|
||||
NoLevelHook, TraceHook, DebugHook, InfoHook, WarnHook, ErrorHook, FatalHook, PanicHook Hook
|
||||
}
|
||||
|
||||
// Run implements the Hook interface.
|
||||
func (h LevelHook) Run(e *Event, level Level, message string) {
|
||||
switch level {
|
||||
case TraceLevel:
|
||||
if h.TraceHook != nil {
|
||||
h.TraceHook.Run(e, level, message)
|
||||
}
|
||||
case DebugLevel:
|
||||
if h.DebugHook != nil {
|
||||
h.DebugHook.Run(e, level, message)
|
||||
}
|
||||
case InfoLevel:
|
||||
if h.InfoHook != nil {
|
||||
h.InfoHook.Run(e, level, message)
|
||||
}
|
||||
case WarnLevel:
|
||||
if h.WarnHook != nil {
|
||||
h.WarnHook.Run(e, level, message)
|
||||
}
|
||||
case ErrorLevel:
|
||||
if h.ErrorHook != nil {
|
||||
h.ErrorHook.Run(e, level, message)
|
||||
}
|
||||
case FatalLevel:
|
||||
if h.FatalHook != nil {
|
||||
h.FatalHook.Run(e, level, message)
|
||||
}
|
||||
case PanicLevel:
|
||||
if h.PanicHook != nil {
|
||||
h.PanicHook.Run(e, level, message)
|
||||
}
|
||||
case NoLevel:
|
||||
if h.NoLevelHook != nil {
|
||||
h.NoLevelHook.Run(e, level, message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewLevelHook returns a new LevelHook.
|
||||
func NewLevelHook() LevelHook {
|
||||
return LevelHook{}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
## Reference:
|
||||
CBOR Encoding is described in [RFC7049](https://tools.ietf.org/html/rfc7049)
|
||||
|
||||
## Comparison of JSON vs CBOR
|
||||
|
||||
Two main areas of reduction are:
|
||||
|
||||
1. CPU usage to write a log msg
|
||||
2. Size (in bytes) of log messages.
|
||||
|
||||
|
||||
CPU Usage savings are below:
|
||||
```
|
||||
name JSON time/op CBOR time/op delta
|
||||
Info-32 15.3ns ± 1% 11.7ns ± 3% -23.78% (p=0.000 n=9+10)
|
||||
ContextFields-32 16.2ns ± 2% 12.3ns ± 3% -23.97% (p=0.000 n=9+9)
|
||||
ContextAppend-32 6.70ns ± 0% 6.20ns ± 0% -7.44% (p=0.000 n=9+9)
|
||||
LogFields-32 66.4ns ± 0% 24.6ns ± 2% -62.89% (p=0.000 n=10+9)
|
||||
LogArrayObject-32 911ns ±11% 768ns ± 6% -15.64% (p=0.000 n=10+10)
|
||||
LogFieldType/Floats-32 70.3ns ± 2% 29.5ns ± 1% -57.98% (p=0.000 n=10+10)
|
||||
LogFieldType/Err-32 14.0ns ± 3% 12.1ns ± 8% -13.20% (p=0.000 n=8+10)
|
||||
LogFieldType/Dur-32 17.2ns ± 2% 13.1ns ± 1% -24.27% (p=0.000 n=10+9)
|
||||
LogFieldType/Object-32 54.3ns ±11% 52.3ns ± 7% ~ (p=0.239 n=10+10)
|
||||
LogFieldType/Ints-32 20.3ns ± 2% 15.1ns ± 2% -25.50% (p=0.000 n=9+10)
|
||||
LogFieldType/Interfaces-32 642ns ±11% 621ns ± 9% ~ (p=0.118 n=10+10)
|
||||
LogFieldType/Interface(Objects)-32 635ns ±13% 632ns ± 9% ~ (p=0.592 n=10+10)
|
||||
LogFieldType/Times-32 294ns ± 0% 27ns ± 1% -90.71% (p=0.000 n=10+9)
|
||||
LogFieldType/Durs-32 121ns ± 0% 33ns ± 2% -72.44% (p=0.000 n=9+9)
|
||||
LogFieldType/Interface(Object)-32 56.6ns ± 8% 52.3ns ± 8% -7.54% (p=0.007 n=10+10)
|
||||
LogFieldType/Errs-32 17.8ns ± 3% 16.1ns ± 2% -9.71% (p=0.000 n=10+9)
|
||||
LogFieldType/Time-32 40.5ns ± 1% 12.7ns ± 6% -68.66% (p=0.000 n=8+9)
|
||||
LogFieldType/Bool-32 12.0ns ± 5% 10.2ns ± 2% -15.18% (p=0.000 n=10+8)
|
||||
LogFieldType/Bools-32 17.2ns ± 2% 12.6ns ± 4% -26.63% (p=0.000 n=10+10)
|
||||
LogFieldType/Int-32 12.3ns ± 2% 11.2ns ± 4% -9.27% (p=0.000 n=9+10)
|
||||
LogFieldType/Float-32 16.7ns ± 1% 12.6ns ± 2% -24.42% (p=0.000 n=7+9)
|
||||
LogFieldType/Str-32 12.7ns ± 7% 11.3ns ± 7% -10.88% (p=0.000 n=10+9)
|
||||
LogFieldType/Strs-32 20.3ns ± 3% 18.2ns ± 3% -10.25% (p=0.000 n=9+10)
|
||||
LogFieldType/Interface-32 183ns ±12% 175ns ± 9% ~ (p=0.078 n=10+10)
|
||||
```
|
||||
|
||||
Log message size savings is greatly dependent on the number and type of fields in the log message.
|
||||
Assuming this log message (with an Integer, timestamp and string, in addition to level).
|
||||
|
||||
`{"level":"error","Fault":41650,"time":"2018-04-01T15:18:19-07:00","message":"Some Message"}`
|
||||
|
||||
Two measurements were done for the log file sizes - one without any compression, second
|
||||
using [compress/zlib](https://golang.org/pkg/compress/zlib/).
|
||||
|
||||
Results for 10,000 log messages:
|
||||
|
||||
| Log Format | Plain File Size (in KB) | Compressed File Size (in KB) |
|
||||
| :--- | :---: | :---: |
|
||||
| JSON | 920 | 28 |
|
||||
| CBOR | 550 | 28 |
|
||||
|
||||
The example used to calculate the above data is available in [Examples](examples).
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package cbor
|
||||
|
||||
// JSONMarshalFunc is used to marshal interface to JSON encoded byte slice.
|
||||
// Making it package level instead of embedded in Encoder brings
|
||||
// some extra efforts at importing, but avoids value copy when the functions
|
||||
// of Encoder being invoked.
|
||||
// DO REMEMBER to set this variable at importing, or
|
||||
// you might get a nil pointer dereference panic at runtime.
|
||||
var JSONMarshalFunc func(v interface{}) ([]byte, error)
|
||||
|
||||
type Encoder struct{}
|
||||
|
||||
// AppendKey adds a key (string) to the binary encoded log message
|
||||
func (e Encoder) AppendKey(dst []byte, key string) []byte {
|
||||
if len(dst) < 1 {
|
||||
dst = e.AppendBeginMarker(dst)
|
||||
}
|
||||
return e.AppendString(dst, key)
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
// Package cbor provides primitives for storing different data
|
||||
// in the CBOR (binary) format. CBOR is defined in RFC7049.
|
||||
package cbor
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
majorOffset = 5
|
||||
additionalMax = 23
|
||||
|
||||
// Non Values.
|
||||
additionalTypeBoolFalse byte = 20
|
||||
additionalTypeBoolTrue byte = 21
|
||||
additionalTypeNull byte = 22
|
||||
|
||||
// Integer (+ve and -ve) Sub-types.
|
||||
additionalTypeIntUint8 byte = 24
|
||||
additionalTypeIntUint16 byte = 25
|
||||
additionalTypeIntUint32 byte = 26
|
||||
additionalTypeIntUint64 byte = 27
|
||||
|
||||
// Float Sub-types.
|
||||
additionalTypeFloat16 byte = 25
|
||||
additionalTypeFloat32 byte = 26
|
||||
additionalTypeFloat64 byte = 27
|
||||
additionalTypeBreak byte = 31
|
||||
|
||||
// Tag Sub-types.
|
||||
additionalTypeTimestamp byte = 01
|
||||
additionalTypeEmbeddedCBOR byte = 63
|
||||
|
||||
// Extended Tags - from https://www.iana.org/assignments/cbor-tags/cbor-tags.xhtml
|
||||
additionalTypeTagNetworkAddr uint16 = 260
|
||||
additionalTypeTagNetworkPrefix uint16 = 261
|
||||
additionalTypeEmbeddedJSON uint16 = 262
|
||||
additionalTypeTagHexString uint16 = 263
|
||||
|
||||
// Unspecified number of elements.
|
||||
additionalTypeInfiniteCount byte = 31
|
||||
)
|
||||
const (
|
||||
majorTypeUnsignedInt byte = iota << majorOffset // Major type 0
|
||||
majorTypeNegativeInt // Major type 1
|
||||
majorTypeByteString // Major type 2
|
||||
majorTypeUtf8String // Major type 3
|
||||
majorTypeArray // Major type 4
|
||||
majorTypeMap // Major type 5
|
||||
majorTypeTags // Major type 6
|
||||
majorTypeSimpleAndFloat // Major type 7
|
||||
)
|
||||
|
||||
const (
|
||||
maskOutAdditionalType byte = (7 << majorOffset)
|
||||
maskOutMajorType byte = 31
|
||||
)
|
||||
|
||||
const (
|
||||
float32Nan = "\x7f\xc0\x00\x00"
|
||||
float32PosInfinity = "\x7f\x80\x00\x00"
|
||||
float32NegInfinity = "\xff\x80\x00\x00"
|
||||
float64Nan = "\x7f\xf8\x00\x00\x00\x00\x00\x00"
|
||||
float64PosInfinity = "\x7f\xf0\x00\x00\x00\x00\x00\x00"
|
||||
float64NegInfinity = "\xff\xf0\x00\x00\x00\x00\x00\x00"
|
||||
)
|
||||
|
||||
// IntegerTimeFieldFormat indicates the format of timestamp decoded
|
||||
// from an integer (time in seconds).
|
||||
var IntegerTimeFieldFormat = time.RFC3339
|
||||
|
||||
// NanoTimeFieldFormat indicates the format of timestamp decoded
|
||||
// from a float value (time in seconds and nanoseconds).
|
||||
var NanoTimeFieldFormat = time.RFC3339Nano
|
||||
|
||||
func appendCborTypePrefix(dst []byte, major byte, number uint64) []byte {
|
||||
var byteCount int
|
||||
var minor byte
|
||||
switch {
|
||||
case number < 256:
|
||||
byteCount = 1
|
||||
minor = additionalTypeIntUint8
|
||||
|
||||
case number < 65536:
|
||||
byteCount = 2
|
||||
minor = additionalTypeIntUint16
|
||||
|
||||
case number < 4294967296:
|
||||
byteCount = 4
|
||||
minor = additionalTypeIntUint32
|
||||
|
||||
default:
|
||||
byteCount = 8
|
||||
minor = additionalTypeIntUint64
|
||||
|
||||
}
|
||||
|
||||
dst = append(dst, major|minor)
|
||||
byteCount--
|
||||
for ; byteCount >= 0; byteCount-- {
|
||||
dst = append(dst, byte(number>>(uint(byteCount)*8)))
|
||||
}
|
||||
return dst
|
||||
}
|
||||
+654
@@ -0,0 +1,654 @@
|
||||
package cbor
|
||||
|
||||
// This file contains code to decode a stream of CBOR Data into JSON.
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"net"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var decodeTimeZone *time.Location
|
||||
|
||||
const hexTable = "0123456789abcdef"
|
||||
|
||||
const isFloat32 = 4
|
||||
const isFloat64 = 8
|
||||
|
||||
func readNBytes(src *bufio.Reader, n int) []byte {
|
||||
ret := make([]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
ch, e := src.ReadByte()
|
||||
if e != nil {
|
||||
panic(fmt.Errorf("Tried to Read %d Bytes.. But hit end of file", n))
|
||||
}
|
||||
ret[i] = ch
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func readByte(src *bufio.Reader) byte {
|
||||
b, e := src.ReadByte()
|
||||
if e != nil {
|
||||
panic(fmt.Errorf("Tried to Read 1 Byte.. But hit end of file"))
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func decodeIntAdditionalType(src *bufio.Reader, minor byte) int64 {
|
||||
val := int64(0)
|
||||
if minor <= 23 {
|
||||
val = int64(minor)
|
||||
} else {
|
||||
bytesToRead := 0
|
||||
switch minor {
|
||||
case additionalTypeIntUint8:
|
||||
bytesToRead = 1
|
||||
case additionalTypeIntUint16:
|
||||
bytesToRead = 2
|
||||
case additionalTypeIntUint32:
|
||||
bytesToRead = 4
|
||||
case additionalTypeIntUint64:
|
||||
bytesToRead = 8
|
||||
default:
|
||||
panic(fmt.Errorf("Invalid Additional Type: %d in decodeInteger (expected <28)", minor))
|
||||
}
|
||||
pb := readNBytes(src, bytesToRead)
|
||||
for i := 0; i < bytesToRead; i++ {
|
||||
val = val * 256
|
||||
val += int64(pb[i])
|
||||
}
|
||||
}
|
||||
return val
|
||||
}
|
||||
|
||||
func decodeInteger(src *bufio.Reader) int64 {
|
||||
pb := readByte(src)
|
||||
major := pb & maskOutAdditionalType
|
||||
minor := pb & maskOutMajorType
|
||||
if major != majorTypeUnsignedInt && major != majorTypeNegativeInt {
|
||||
panic(fmt.Errorf("Major type is: %d in decodeInteger!! (expected 0 or 1)", major))
|
||||
}
|
||||
val := decodeIntAdditionalType(src, minor)
|
||||
if major == 0 {
|
||||
return val
|
||||
}
|
||||
return (-1 - val)
|
||||
}
|
||||
|
||||
func decodeFloat(src *bufio.Reader) (float64, int) {
|
||||
pb := readByte(src)
|
||||
major := pb & maskOutAdditionalType
|
||||
minor := pb & maskOutMajorType
|
||||
if major != majorTypeSimpleAndFloat {
|
||||
panic(fmt.Errorf("Incorrect Major type is: %d in decodeFloat", major))
|
||||
}
|
||||
|
||||
switch minor {
|
||||
case additionalTypeFloat16:
|
||||
panic(fmt.Errorf("float16 is not supported in decodeFloat"))
|
||||
|
||||
case additionalTypeFloat32:
|
||||
pb := readNBytes(src, 4)
|
||||
switch string(pb) {
|
||||
case float32Nan:
|
||||
return math.NaN(), isFloat32
|
||||
case float32PosInfinity:
|
||||
return math.Inf(0), isFloat32
|
||||
case float32NegInfinity:
|
||||
return math.Inf(-1), isFloat32
|
||||
}
|
||||
n := uint32(0)
|
||||
for i := 0; i < 4; i++ {
|
||||
n = n * 256
|
||||
n += uint32(pb[i])
|
||||
}
|
||||
val := math.Float32frombits(n)
|
||||
return float64(val), isFloat32
|
||||
case additionalTypeFloat64:
|
||||
pb := readNBytes(src, 8)
|
||||
switch string(pb) {
|
||||
case float64Nan:
|
||||
return math.NaN(), isFloat64
|
||||
case float64PosInfinity:
|
||||
return math.Inf(0), isFloat64
|
||||
case float64NegInfinity:
|
||||
return math.Inf(-1), isFloat64
|
||||
}
|
||||
n := uint64(0)
|
||||
for i := 0; i < 8; i++ {
|
||||
n = n * 256
|
||||
n += uint64(pb[i])
|
||||
}
|
||||
val := math.Float64frombits(n)
|
||||
return val, isFloat64
|
||||
}
|
||||
panic(fmt.Errorf("Invalid Additional Type: %d in decodeFloat", minor))
|
||||
}
|
||||
|
||||
func decodeStringComplex(dst []byte, s string, pos uint) []byte {
|
||||
i := int(pos)
|
||||
start := 0
|
||||
|
||||
for i < len(s) {
|
||||
b := s[i]
|
||||
if b >= utf8.RuneSelf {
|
||||
r, size := utf8.DecodeRuneInString(s[i:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
// In case of error, first append previous simple characters to
|
||||
// the byte slice if any and append a replacement character code
|
||||
// in place of the invalid sequence.
|
||||
if start < i {
|
||||
dst = append(dst, s[start:i]...)
|
||||
}
|
||||
dst = append(dst, `\ufffd`...)
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
i += size
|
||||
continue
|
||||
}
|
||||
if b >= 0x20 && b <= 0x7e && b != '\\' && b != '"' {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
// We encountered a character that needs to be encoded.
|
||||
// Let's append the previous simple characters to the byte slice
|
||||
// and switch our operation to read and encode the remainder
|
||||
// characters byte-by-byte.
|
||||
if start < i {
|
||||
dst = append(dst, s[start:i]...)
|
||||
}
|
||||
switch b {
|
||||
case '"', '\\':
|
||||
dst = append(dst, '\\', b)
|
||||
case '\b':
|
||||
dst = append(dst, '\\', 'b')
|
||||
case '\f':
|
||||
dst = append(dst, '\\', 'f')
|
||||
case '\n':
|
||||
dst = append(dst, '\\', 'n')
|
||||
case '\r':
|
||||
dst = append(dst, '\\', 'r')
|
||||
case '\t':
|
||||
dst = append(dst, '\\', 't')
|
||||
default:
|
||||
dst = append(dst, '\\', 'u', '0', '0', hexTable[b>>4], hexTable[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
}
|
||||
if start < len(s) {
|
||||
dst = append(dst, s[start:]...)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
func decodeString(src *bufio.Reader, noQuotes bool) []byte {
|
||||
pb := readByte(src)
|
||||
major := pb & maskOutAdditionalType
|
||||
minor := pb & maskOutMajorType
|
||||
if major != majorTypeByteString {
|
||||
panic(fmt.Errorf("Major type is: %d in decodeString", major))
|
||||
}
|
||||
result := []byte{}
|
||||
if !noQuotes {
|
||||
result = append(result, '"')
|
||||
}
|
||||
length := decodeIntAdditionalType(src, minor)
|
||||
len := int(length)
|
||||
pbs := readNBytes(src, len)
|
||||
result = append(result, pbs...)
|
||||
if noQuotes {
|
||||
return result
|
||||
}
|
||||
return append(result, '"')
|
||||
}
|
||||
func decodeStringToDataUrl(src *bufio.Reader, mimeType string) []byte {
|
||||
pb := readByte(src)
|
||||
major := pb & maskOutAdditionalType
|
||||
minor := pb & maskOutMajorType
|
||||
if major != majorTypeByteString {
|
||||
panic(fmt.Errorf("Major type is: %d in decodeString", major))
|
||||
}
|
||||
length := decodeIntAdditionalType(src, minor)
|
||||
l := int(length)
|
||||
enc := base64.StdEncoding
|
||||
lEnc := enc.EncodedLen(l)
|
||||
result := make([]byte, len("\"data:;base64,\"")+len(mimeType)+lEnc)
|
||||
dest := result
|
||||
u := copy(dest, "\"data:")
|
||||
dest = dest[u:]
|
||||
u = copy(dest, mimeType)
|
||||
dest = dest[u:]
|
||||
u = copy(dest, ";base64,")
|
||||
dest = dest[u:]
|
||||
pbs := readNBytes(src, l)
|
||||
enc.Encode(dest, pbs)
|
||||
dest = dest[lEnc:]
|
||||
dest[0] = '"'
|
||||
return result
|
||||
}
|
||||
|
||||
func decodeUTF8String(src *bufio.Reader) []byte {
|
||||
pb := readByte(src)
|
||||
major := pb & maskOutAdditionalType
|
||||
minor := pb & maskOutMajorType
|
||||
if major != majorTypeUtf8String {
|
||||
panic(fmt.Errorf("Major type is: %d in decodeUTF8String", major))
|
||||
}
|
||||
result := []byte{'"'}
|
||||
length := decodeIntAdditionalType(src, minor)
|
||||
len := int(length)
|
||||
pbs := readNBytes(src, len)
|
||||
|
||||
for i := 0; i < len; i++ {
|
||||
// Check if the character needs encoding. Control characters, slashes,
|
||||
// and the double quote need json encoding. Bytes above the ascii
|
||||
// boundary needs utf8 encoding.
|
||||
if pbs[i] < 0x20 || pbs[i] > 0x7e || pbs[i] == '\\' || pbs[i] == '"' {
|
||||
// We encountered a character that needs to be encoded. Switch
|
||||
// to complex version of the algorithm.
|
||||
dst := []byte{'"'}
|
||||
dst = decodeStringComplex(dst, string(pbs), uint(i))
|
||||
return append(dst, '"')
|
||||
}
|
||||
}
|
||||
// The string has no need for encoding and therefore is directly
|
||||
// appended to the byte slice.
|
||||
result = append(result, pbs...)
|
||||
return append(result, '"')
|
||||
}
|
||||
|
||||
func array2Json(src *bufio.Reader, dst io.Writer) {
|
||||
dst.Write([]byte{'['})
|
||||
pb := readByte(src)
|
||||
major := pb & maskOutAdditionalType
|
||||
minor := pb & maskOutMajorType
|
||||
if major != majorTypeArray {
|
||||
panic(fmt.Errorf("Major type is: %d in array2Json", major))
|
||||
}
|
||||
len := 0
|
||||
unSpecifiedCount := false
|
||||
if minor == additionalTypeInfiniteCount {
|
||||
unSpecifiedCount = true
|
||||
} else {
|
||||
length := decodeIntAdditionalType(src, minor)
|
||||
len = int(length)
|
||||
}
|
||||
for i := 0; unSpecifiedCount || i < len; i++ {
|
||||
if unSpecifiedCount {
|
||||
pb, e := src.Peek(1)
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
if pb[0] == majorTypeSimpleAndFloat|additionalTypeBreak {
|
||||
readByte(src)
|
||||
break
|
||||
}
|
||||
}
|
||||
cbor2JsonOneObject(src, dst)
|
||||
if unSpecifiedCount {
|
||||
pb, e := src.Peek(1)
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
if pb[0] == majorTypeSimpleAndFloat|additionalTypeBreak {
|
||||
readByte(src)
|
||||
break
|
||||
}
|
||||
dst.Write([]byte{','})
|
||||
} else if i+1 < len {
|
||||
dst.Write([]byte{','})
|
||||
}
|
||||
}
|
||||
dst.Write([]byte{']'})
|
||||
}
|
||||
|
||||
func map2Json(src *bufio.Reader, dst io.Writer) {
|
||||
pb := readByte(src)
|
||||
major := pb & maskOutAdditionalType
|
||||
minor := pb & maskOutMajorType
|
||||
if major != majorTypeMap {
|
||||
panic(fmt.Errorf("Major type is: %d in map2Json", major))
|
||||
}
|
||||
len := 0
|
||||
unSpecifiedCount := false
|
||||
if minor == additionalTypeInfiniteCount {
|
||||
unSpecifiedCount = true
|
||||
} else {
|
||||
length := decodeIntAdditionalType(src, minor)
|
||||
len = int(length)
|
||||
}
|
||||
dst.Write([]byte{'{'})
|
||||
for i := 0; unSpecifiedCount || i < len; i++ {
|
||||
if unSpecifiedCount {
|
||||
pb, e := src.Peek(1)
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
if pb[0] == majorTypeSimpleAndFloat|additionalTypeBreak {
|
||||
readByte(src)
|
||||
break
|
||||
}
|
||||
}
|
||||
cbor2JsonOneObject(src, dst)
|
||||
if i%2 == 0 {
|
||||
// Even position values are keys.
|
||||
dst.Write([]byte{':'})
|
||||
} else {
|
||||
if unSpecifiedCount {
|
||||
pb, e := src.Peek(1)
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
if pb[0] == majorTypeSimpleAndFloat|additionalTypeBreak {
|
||||
readByte(src)
|
||||
break
|
||||
}
|
||||
dst.Write([]byte{','})
|
||||
} else if i+1 < len {
|
||||
dst.Write([]byte{','})
|
||||
}
|
||||
}
|
||||
}
|
||||
dst.Write([]byte{'}'})
|
||||
}
|
||||
|
||||
func decodeTagData(src *bufio.Reader) []byte {
|
||||
pb := readByte(src)
|
||||
major := pb & maskOutAdditionalType
|
||||
minor := pb & maskOutMajorType
|
||||
if major != majorTypeTags {
|
||||
panic(fmt.Errorf("Major type is: %d in decodeTagData", major))
|
||||
}
|
||||
switch minor {
|
||||
case additionalTypeTimestamp:
|
||||
return decodeTimeStamp(src)
|
||||
case additionalTypeIntUint8:
|
||||
val := decodeIntAdditionalType(src, minor)
|
||||
switch byte(val) {
|
||||
case additionalTypeEmbeddedCBOR:
|
||||
pb := readByte(src)
|
||||
dataMajor := pb & maskOutAdditionalType
|
||||
if dataMajor != majorTypeByteString {
|
||||
panic(fmt.Errorf("Unsupported embedded Type: %d in decodeEmbeddedCBOR", dataMajor))
|
||||
}
|
||||
src.UnreadByte()
|
||||
return decodeStringToDataUrl(src, "application/cbor")
|
||||
default:
|
||||
panic(fmt.Errorf("Unsupported Additional Tag Type: %d in decodeTagData", val))
|
||||
}
|
||||
|
||||
// Tag value is larger than 256 (so uint16).
|
||||
case additionalTypeIntUint16:
|
||||
val := decodeIntAdditionalType(src, minor)
|
||||
|
||||
switch uint16(val) {
|
||||
case additionalTypeEmbeddedJSON:
|
||||
pb := readByte(src)
|
||||
dataMajor := pb & maskOutAdditionalType
|
||||
if dataMajor != majorTypeByteString {
|
||||
panic(fmt.Errorf("Unsupported embedded Type: %d in decodeEmbeddedJSON", dataMajor))
|
||||
}
|
||||
src.UnreadByte()
|
||||
return decodeString(src, true)
|
||||
|
||||
case additionalTypeTagNetworkAddr:
|
||||
octets := decodeString(src, true)
|
||||
ss := []byte{'"'}
|
||||
switch len(octets) {
|
||||
case 6: // MAC address.
|
||||
ha := net.HardwareAddr(octets)
|
||||
ss = append(append(ss, ha.String()...), '"')
|
||||
case 4: // IPv4 address.
|
||||
fallthrough
|
||||
case 16: // IPv6 address.
|
||||
ip := net.IP(octets)
|
||||
ss = append(append(ss, ip.String()...), '"')
|
||||
default:
|
||||
panic(fmt.Errorf("Unexpected Network Address length: %d (expected 4,6,16)", len(octets)))
|
||||
}
|
||||
return ss
|
||||
|
||||
case additionalTypeTagNetworkPrefix:
|
||||
pb := readByte(src)
|
||||
if pb != majorTypeMap|0x1 {
|
||||
panic(fmt.Errorf("IP Prefix is NOT of MAP of 1 elements as expected"))
|
||||
}
|
||||
octets := decodeString(src, true)
|
||||
val := decodeInteger(src)
|
||||
ip := net.IP(octets)
|
||||
var mask net.IPMask
|
||||
pfxLen := int(val)
|
||||
if len(octets) == 4 {
|
||||
mask = net.CIDRMask(pfxLen, 32)
|
||||
} else {
|
||||
mask = net.CIDRMask(pfxLen, 128)
|
||||
}
|
||||
ipPfx := net.IPNet{IP: ip, Mask: mask}
|
||||
ss := []byte{'"'}
|
||||
ss = append(append(ss, ipPfx.String()...), '"')
|
||||
return ss
|
||||
|
||||
case additionalTypeTagHexString:
|
||||
octets := decodeString(src, true)
|
||||
ss := []byte{'"'}
|
||||
for _, v := range octets {
|
||||
ss = append(ss, hexTable[v>>4], hexTable[v&0x0f])
|
||||
}
|
||||
return append(ss, '"')
|
||||
|
||||
default:
|
||||
panic(fmt.Errorf("Unsupported Additional Tag Type: %d in decodeTagData", val))
|
||||
}
|
||||
}
|
||||
panic(fmt.Errorf("Unsupported Additional Type: %d in decodeTagData", minor))
|
||||
}
|
||||
|
||||
func decodeTimeStamp(src *bufio.Reader) []byte {
|
||||
pb := readByte(src)
|
||||
src.UnreadByte()
|
||||
tsMajor := pb & maskOutAdditionalType
|
||||
if tsMajor == majorTypeUnsignedInt || tsMajor == majorTypeNegativeInt {
|
||||
n := decodeInteger(src)
|
||||
t := time.Unix(n, 0)
|
||||
if decodeTimeZone != nil {
|
||||
t = t.In(decodeTimeZone)
|
||||
} else {
|
||||
t = t.In(time.UTC)
|
||||
}
|
||||
tsb := []byte{}
|
||||
tsb = append(tsb, '"')
|
||||
tsb = t.AppendFormat(tsb, IntegerTimeFieldFormat)
|
||||
tsb = append(tsb, '"')
|
||||
return tsb
|
||||
} else if tsMajor == majorTypeSimpleAndFloat {
|
||||
n, _ := decodeFloat(src)
|
||||
secs := int64(n)
|
||||
n -= float64(secs)
|
||||
n *= float64(1e9)
|
||||
t := time.Unix(secs, int64(n))
|
||||
if decodeTimeZone != nil {
|
||||
t = t.In(decodeTimeZone)
|
||||
} else {
|
||||
t = t.In(time.UTC)
|
||||
}
|
||||
tsb := []byte{}
|
||||
tsb = append(tsb, '"')
|
||||
tsb = t.AppendFormat(tsb, NanoTimeFieldFormat)
|
||||
tsb = append(tsb, '"')
|
||||
return tsb
|
||||
}
|
||||
panic(fmt.Errorf("TS format is neither int nor float: %d", tsMajor))
|
||||
}
|
||||
|
||||
func decodeSimpleFloat(src *bufio.Reader) []byte {
|
||||
pb := readByte(src)
|
||||
major := pb & maskOutAdditionalType
|
||||
minor := pb & maskOutMajorType
|
||||
if major != majorTypeSimpleAndFloat {
|
||||
panic(fmt.Errorf("Major type is: %d in decodeSimpleFloat", major))
|
||||
}
|
||||
switch minor {
|
||||
case additionalTypeBoolTrue:
|
||||
return []byte("true")
|
||||
case additionalTypeBoolFalse:
|
||||
return []byte("false")
|
||||
case additionalTypeNull:
|
||||
return []byte("null")
|
||||
case additionalTypeFloat16:
|
||||
fallthrough
|
||||
case additionalTypeFloat32:
|
||||
fallthrough
|
||||
case additionalTypeFloat64:
|
||||
src.UnreadByte()
|
||||
v, bc := decodeFloat(src)
|
||||
ba := []byte{}
|
||||
switch {
|
||||
case math.IsNaN(v):
|
||||
return []byte("\"NaN\"")
|
||||
case math.IsInf(v, 1):
|
||||
return []byte("\"+Inf\"")
|
||||
case math.IsInf(v, -1):
|
||||
return []byte("\"-Inf\"")
|
||||
}
|
||||
if bc == isFloat32 {
|
||||
ba = strconv.AppendFloat(ba, v, 'f', -1, 32)
|
||||
} else if bc == isFloat64 {
|
||||
ba = strconv.AppendFloat(ba, v, 'f', -1, 64)
|
||||
} else {
|
||||
panic(fmt.Errorf("Invalid Float precision from decodeFloat: %d", bc))
|
||||
}
|
||||
return ba
|
||||
default:
|
||||
panic(fmt.Errorf("Invalid Additional Type: %d in decodeSimpleFloat", minor))
|
||||
}
|
||||
}
|
||||
|
||||
func cbor2JsonOneObject(src *bufio.Reader, dst io.Writer) {
|
||||
pb, e := src.Peek(1)
|
||||
if e != nil {
|
||||
panic(e)
|
||||
}
|
||||
major := (pb[0] & maskOutAdditionalType)
|
||||
|
||||
switch major {
|
||||
case majorTypeUnsignedInt:
|
||||
fallthrough
|
||||
case majorTypeNegativeInt:
|
||||
n := decodeInteger(src)
|
||||
dst.Write([]byte(strconv.Itoa(int(n))))
|
||||
|
||||
case majorTypeByteString:
|
||||
s := decodeString(src, false)
|
||||
dst.Write(s)
|
||||
|
||||
case majorTypeUtf8String:
|
||||
s := decodeUTF8String(src)
|
||||
dst.Write(s)
|
||||
|
||||
case majorTypeArray:
|
||||
array2Json(src, dst)
|
||||
|
||||
case majorTypeMap:
|
||||
map2Json(src, dst)
|
||||
|
||||
case majorTypeTags:
|
||||
s := decodeTagData(src)
|
||||
dst.Write(s)
|
||||
|
||||
case majorTypeSimpleAndFloat:
|
||||
s := decodeSimpleFloat(src)
|
||||
dst.Write(s)
|
||||
}
|
||||
}
|
||||
|
||||
func moreBytesToRead(src *bufio.Reader) bool {
|
||||
_, e := src.ReadByte()
|
||||
if e == nil {
|
||||
src.UnreadByte()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Cbor2JsonManyObjects decodes all the CBOR Objects read from src
|
||||
// reader. It keeps on decoding until reader returns EOF (error when reading).
|
||||
// Decoded string is written to the dst. At the end of every CBOR Object
|
||||
// newline is written to the output stream.
|
||||
//
|
||||
// Returns error (if any) that was encountered during decode.
|
||||
// The child functions will generate a panic when error is encountered and
|
||||
// this function will recover non-runtime Errors and return the reason as error.
|
||||
func Cbor2JsonManyObjects(src io.Reader, dst io.Writer) (err error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if _, ok := r.(runtime.Error); ok {
|
||||
panic(r)
|
||||
}
|
||||
err = r.(error)
|
||||
}
|
||||
}()
|
||||
bufRdr := bufio.NewReader(src)
|
||||
for moreBytesToRead(bufRdr) {
|
||||
cbor2JsonOneObject(bufRdr, dst)
|
||||
dst.Write([]byte("\n"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Detect if the bytes to be printed is Binary or not.
|
||||
func binaryFmt(p []byte) bool {
|
||||
if len(p) > 0 && p[0] > 0x7F {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getReader(str string) *bufio.Reader {
|
||||
return bufio.NewReader(strings.NewReader(str))
|
||||
}
|
||||
|
||||
// DecodeIfBinaryToString converts a binary formatted log msg to a
|
||||
// JSON formatted String Log message - suitable for printing to Console/Syslog.
|
||||
func DecodeIfBinaryToString(in []byte) string {
|
||||
if binaryFmt(in) {
|
||||
var b bytes.Buffer
|
||||
Cbor2JsonManyObjects(strings.NewReader(string(in)), &b)
|
||||
return b.String()
|
||||
}
|
||||
return string(in)
|
||||
}
|
||||
|
||||
// DecodeObjectToStr checks if the input is a binary format, if so,
|
||||
// it will decode a single Object and return the decoded string.
|
||||
func DecodeObjectToStr(in []byte) string {
|
||||
if binaryFmt(in) {
|
||||
var b bytes.Buffer
|
||||
cbor2JsonOneObject(getReader(string(in)), &b)
|
||||
return b.String()
|
||||
}
|
||||
return string(in)
|
||||
}
|
||||
|
||||
// DecodeIfBinaryToBytes checks if the input is a binary format, if so,
|
||||
// it will decode all Objects and return the decoded string as byte array.
|
||||
func DecodeIfBinaryToBytes(in []byte) []byte {
|
||||
if binaryFmt(in) {
|
||||
var b bytes.Buffer
|
||||
Cbor2JsonManyObjects(bytes.NewReader(in), &b)
|
||||
return b.Bytes()
|
||||
}
|
||||
return in
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package cbor
|
||||
|
||||
import "fmt"
|
||||
|
||||
// AppendStrings encodes and adds an array of strings to the dst byte array.
|
||||
func (e Encoder) AppendStrings(dst []byte, vals []string) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendString(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendString encodes and adds a string to the dst byte array.
|
||||
func (Encoder) AppendString(dst []byte, s string) []byte {
|
||||
major := majorTypeUtf8String
|
||||
|
||||
l := len(s)
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, majorTypeUtf8String, uint64(l))
|
||||
}
|
||||
return append(dst, s...)
|
||||
}
|
||||
|
||||
// AppendStringers encodes and adds an array of Stringer values
|
||||
// to the dst byte array.
|
||||
func (e Encoder) AppendStringers(dst []byte, vals []fmt.Stringer) []byte {
|
||||
if vals == nil || len(vals) == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
dst = e.AppendArrayStart(dst)
|
||||
dst = e.AppendStringer(dst, vals[0])
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = e.AppendStringer(dst, val)
|
||||
}
|
||||
}
|
||||
return e.AppendArrayEnd(dst)
|
||||
}
|
||||
|
||||
// AppendStringer encodes and adds the Stringer value to the dst
|
||||
// byte array.
|
||||
func (e Encoder) AppendStringer(dst []byte, val fmt.Stringer) []byte {
|
||||
if val == nil {
|
||||
return e.AppendNil(dst)
|
||||
}
|
||||
return e.AppendString(dst, val.String())
|
||||
}
|
||||
|
||||
// AppendBytes encodes and adds an array of bytes to the dst byte array.
|
||||
func (Encoder) AppendBytes(dst, s []byte) []byte {
|
||||
major := majorTypeByteString
|
||||
|
||||
l := len(s)
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
return append(dst, s...)
|
||||
}
|
||||
|
||||
// AppendEmbeddedJSON adds a tag and embeds input JSON as such.
|
||||
func AppendEmbeddedJSON(dst, s []byte) []byte {
|
||||
major := majorTypeTags
|
||||
minor := additionalTypeEmbeddedJSON
|
||||
|
||||
// Append the TAG to indicate this is Embedded JSON.
|
||||
dst = append(dst, major|additionalTypeIntUint16)
|
||||
dst = append(dst, byte(minor>>8))
|
||||
dst = append(dst, byte(minor&0xff))
|
||||
|
||||
// Append the JSON Object as Byte String.
|
||||
major = majorTypeByteString
|
||||
|
||||
l := len(s)
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
return append(dst, s...)
|
||||
}
|
||||
|
||||
// AppendEmbeddedCBOR adds a tag and embeds input CBOR as such.
|
||||
func AppendEmbeddedCBOR(dst, s []byte) []byte {
|
||||
major := majorTypeTags
|
||||
minor := additionalTypeEmbeddedCBOR
|
||||
|
||||
// Append the TAG to indicate this is Embedded JSON.
|
||||
dst = append(dst, major|additionalTypeIntUint8)
|
||||
dst = append(dst, minor)
|
||||
|
||||
// Append the CBOR Object as Byte String.
|
||||
major = majorTypeByteString
|
||||
|
||||
l := len(s)
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
return append(dst, s...)
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package cbor
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Import from zerolog/global.go
|
||||
timeFormatUnix = ""
|
||||
timeFormatUnixMs = "UNIXMS"
|
||||
timeFormatUnixMicro = "UNIXMICRO"
|
||||
timeFormatUnixNano = "UNIXNANO"
|
||||
durationFormatFloat = "float"
|
||||
durationFormatInt = "int"
|
||||
durationFormatString = "string"
|
||||
)
|
||||
|
||||
func appendIntegerTimestamp(dst []byte, t time.Time) []byte {
|
||||
major := majorTypeTags
|
||||
minor := additionalTypeTimestamp
|
||||
dst = append(dst, major|minor)
|
||||
secs := t.Unix()
|
||||
var val uint64
|
||||
if secs < 0 {
|
||||
major = majorTypeNegativeInt
|
||||
val = uint64(-secs - 1)
|
||||
} else {
|
||||
major = majorTypeUnsignedInt
|
||||
val = uint64(secs)
|
||||
}
|
||||
dst = appendCborTypePrefix(dst, major, val)
|
||||
return dst
|
||||
}
|
||||
|
||||
func (e Encoder) appendFloatTimestamp(dst []byte, t time.Time) []byte {
|
||||
major := majorTypeTags
|
||||
minor := additionalTypeTimestamp
|
||||
dst = append(dst, major|minor)
|
||||
secs := t.Unix()
|
||||
nanos := t.Nanosecond()
|
||||
val := float64(secs)*1.0 + float64(nanos)*1e-9
|
||||
return e.AppendFloat64(dst, val, -1)
|
||||
}
|
||||
|
||||
// AppendTime encodes and adds a timestamp to the dst byte array.
|
||||
func (e Encoder) AppendTime(dst []byte, t time.Time, unused string) []byte {
|
||||
utc := t.UTC()
|
||||
if utc.Nanosecond() == 0 {
|
||||
return appendIntegerTimestamp(dst, utc)
|
||||
}
|
||||
return e.appendFloatTimestamp(dst, utc)
|
||||
}
|
||||
|
||||
// AppendTimes encodes and adds an array of timestamps to the dst byte array.
|
||||
func (e Encoder) AppendTimes(dst []byte, vals []time.Time, unused string) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
|
||||
for _, t := range vals {
|
||||
dst = e.AppendTime(dst, t, unused)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendDuration encodes and adds a duration to the dst byte array.
|
||||
// useInt field indicates whether to store the duration as seconds (integer) or
|
||||
// as seconds+nanoseconds (float).
|
||||
func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, format string, useInt bool, unused int) []byte {
|
||||
if useInt {
|
||||
return e.AppendInt64(dst, int64(d/unit))
|
||||
}
|
||||
switch format {
|
||||
case durationFormatFloat:
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit), unused)
|
||||
case durationFormatInt:
|
||||
return e.AppendInt64(dst, int64(d/unit))
|
||||
case durationFormatString:
|
||||
return e.AppendString(dst, d.String())
|
||||
}
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit), unused)
|
||||
}
|
||||
|
||||
// AppendDurations encodes and adds an array of durations to the dst byte array.
|
||||
// useInt field indicates whether to store the duration as seconds (integer) or
|
||||
// as seconds+nanoseconds (float).
|
||||
func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, format string, useInt bool, unused int) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, d := range vals {
|
||||
dst = e.AppendDuration(dst, d, unit, format, useInt, unused)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
+524
@@ -0,0 +1,524 @@
|
||||
package cbor
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// AppendNil inserts a 'Nil' object into the dst byte array.
|
||||
func (Encoder) AppendNil(dst []byte) []byte {
|
||||
return append(dst, majorTypeSimpleAndFloat|additionalTypeNull)
|
||||
}
|
||||
|
||||
// AppendBeginMarker inserts a map start into the dst byte array.
|
||||
func (Encoder) AppendBeginMarker(dst []byte) []byte {
|
||||
return append(dst, majorTypeMap|additionalTypeInfiniteCount)
|
||||
}
|
||||
|
||||
// AppendEndMarker inserts a map end into the dst byte array.
|
||||
func (Encoder) AppendEndMarker(dst []byte) []byte {
|
||||
return append(dst, majorTypeSimpleAndFloat|additionalTypeBreak)
|
||||
}
|
||||
|
||||
// AppendObjectData takes an object in form of a byte array and appends to dst.
|
||||
func (Encoder) AppendObjectData(dst []byte, o []byte) []byte {
|
||||
// BeginMarker is present in the dst, which
|
||||
// should not be copied when appending to existing data.
|
||||
return append(dst, o[1:]...)
|
||||
}
|
||||
|
||||
// AppendArrayStart adds markers to indicate the start of an array.
|
||||
func (Encoder) AppendArrayStart(dst []byte) []byte {
|
||||
return append(dst, majorTypeArray|additionalTypeInfiniteCount)
|
||||
}
|
||||
|
||||
// AppendArrayEnd adds markers to indicate the end of an array.
|
||||
func (Encoder) AppendArrayEnd(dst []byte) []byte {
|
||||
return append(dst, majorTypeSimpleAndFloat|additionalTypeBreak)
|
||||
}
|
||||
|
||||
// AppendArrayDelim adds markers to indicate end of a particular array element.
|
||||
func (Encoder) AppendArrayDelim(dst []byte) []byte {
|
||||
//No delimiters needed in cbor
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendLineBreak is a noop that keep API compat with json encoder.
|
||||
func (Encoder) AppendLineBreak(dst []byte) []byte {
|
||||
// No line breaks needed in binary format.
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendBool encodes and inserts a boolean value into the dst byte array.
|
||||
func (Encoder) AppendBool(dst []byte, val bool) []byte {
|
||||
b := additionalTypeBoolFalse
|
||||
if val {
|
||||
b = additionalTypeBoolTrue
|
||||
}
|
||||
return append(dst, majorTypeSimpleAndFloat|b)
|
||||
}
|
||||
|
||||
// AppendBools encodes and inserts an array of boolean values into the dst byte array.
|
||||
func (e Encoder) AppendBools(dst []byte, vals []bool) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendBool(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInt encodes and inserts an integer value into the dst byte array.
|
||||
func (Encoder) AppendInt(dst []byte, val int) []byte {
|
||||
major := majorTypeUnsignedInt
|
||||
contentVal := val
|
||||
if val < 0 {
|
||||
major = majorTypeNegativeInt
|
||||
contentVal = -val - 1
|
||||
}
|
||||
if contentVal <= additionalMax {
|
||||
lb := byte(contentVal)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(contentVal))
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInts encodes and inserts an array of integer values into the dst byte array.
|
||||
func (e Encoder) AppendInts(dst []byte, vals []int) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendInt(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInt8 encodes and inserts an int8 value into the dst byte array.
|
||||
func (e Encoder) AppendInt8(dst []byte, val int8) []byte {
|
||||
return e.AppendInt(dst, int(val))
|
||||
}
|
||||
|
||||
// AppendInts8 encodes and inserts an array of integer values into the dst byte array.
|
||||
func (e Encoder) AppendInts8(dst []byte, vals []int8) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendInt(dst, int(v))
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInt16 encodes and inserts a int16 value into the dst byte array.
|
||||
func (e Encoder) AppendInt16(dst []byte, val int16) []byte {
|
||||
return e.AppendInt(dst, int(val))
|
||||
}
|
||||
|
||||
// AppendInts16 encodes and inserts an array of int16 values into the dst byte array.
|
||||
func (e Encoder) AppendInts16(dst []byte, vals []int16) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendInt(dst, int(v))
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInt32 encodes and inserts a int32 value into the dst byte array.
|
||||
func (e Encoder) AppendInt32(dst []byte, val int32) []byte {
|
||||
return e.AppendInt(dst, int(val))
|
||||
}
|
||||
|
||||
// AppendInts32 encodes and inserts an array of int32 values into the dst byte array.
|
||||
func (e Encoder) AppendInts32(dst []byte, vals []int32) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendInt(dst, int(v))
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInt64 encodes and inserts a int64 value into the dst byte array.
|
||||
func (Encoder) AppendInt64(dst []byte, val int64) []byte {
|
||||
major := majorTypeUnsignedInt
|
||||
contentVal := val
|
||||
if val < 0 {
|
||||
major = majorTypeNegativeInt
|
||||
contentVal = -val - 1
|
||||
}
|
||||
if contentVal <= additionalMax {
|
||||
lb := byte(contentVal)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(contentVal))
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInts64 encodes and inserts an array of int64 values into the dst byte array.
|
||||
func (e Encoder) AppendInts64(dst []byte, vals []int64) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendInt64(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendUint encodes and inserts an unsigned integer value into the dst byte array.
|
||||
func (e Encoder) AppendUint(dst []byte, val uint) []byte {
|
||||
return e.AppendInt64(dst, int64(val))
|
||||
}
|
||||
|
||||
// AppendUints encodes and inserts an array of unsigned integer values into the dst byte array.
|
||||
func (e Encoder) AppendUints(dst []byte, vals []uint) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendUint(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendUint8 encodes and inserts a unsigned int8 value into the dst byte array.
|
||||
func (e Encoder) AppendUint8(dst []byte, val uint8) []byte {
|
||||
return e.AppendUint(dst, uint(val))
|
||||
}
|
||||
|
||||
// AppendUints8 encodes and inserts an array of uint8 values into the dst byte array.
|
||||
func (e Encoder) AppendUints8(dst []byte, vals []uint8) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendUint8(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendUint16 encodes and inserts a uint16 value into the dst byte array.
|
||||
func (e Encoder) AppendUint16(dst []byte, val uint16) []byte {
|
||||
return e.AppendUint(dst, uint(val))
|
||||
}
|
||||
|
||||
// AppendUints16 encodes and inserts an array of uint16 values into the dst byte array.
|
||||
func (e Encoder) AppendUints16(dst []byte, vals []uint16) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendUint16(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendUint32 encodes and inserts a uint32 value into the dst byte array.
|
||||
func (e Encoder) AppendUint32(dst []byte, val uint32) []byte {
|
||||
return e.AppendUint(dst, uint(val))
|
||||
}
|
||||
|
||||
// AppendUints32 encodes and inserts an array of uint32 values into the dst byte array.
|
||||
func (e Encoder) AppendUints32(dst []byte, vals []uint32) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendUint32(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendUint64 encodes and inserts a uint64 value into the dst byte array.
|
||||
func (Encoder) AppendUint64(dst []byte, val uint64) []byte {
|
||||
major := majorTypeUnsignedInt
|
||||
contentVal := val
|
||||
if contentVal <= additionalMax {
|
||||
lb := byte(contentVal)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, contentVal)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendUints64 encodes and inserts an array of uint64 values into the dst byte array.
|
||||
func (e Encoder) AppendUints64(dst []byte, vals []uint64) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendUint64(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendFloat32 encodes and inserts a single precision float value into the dst byte array.
|
||||
func (Encoder) AppendFloat32(dst []byte, val float32, unused int) []byte {
|
||||
switch {
|
||||
case math.IsNaN(float64(val)):
|
||||
return append(dst, "\xfa\x7f\xc0\x00\x00"...)
|
||||
case math.IsInf(float64(val), 1):
|
||||
return append(dst, "\xfa\x7f\x80\x00\x00"...)
|
||||
case math.IsInf(float64(val), -1):
|
||||
return append(dst, "\xfa\xff\x80\x00\x00"...)
|
||||
}
|
||||
major := majorTypeSimpleAndFloat
|
||||
subType := additionalTypeFloat32
|
||||
n := math.Float32bits(val)
|
||||
var buf [4]byte
|
||||
for i := uint(0); i < 4; i++ {
|
||||
buf[i] = byte(n >> ((3 - i) * 8))
|
||||
}
|
||||
return append(append(dst, major|subType), buf[0], buf[1], buf[2], buf[3])
|
||||
}
|
||||
|
||||
// AppendFloats32 encodes and inserts an array of single precision float value into the dst byte array.
|
||||
func (e Encoder) AppendFloats32(dst []byte, vals []float32, unused int) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendFloat32(dst, v, unused)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendFloat64 encodes and inserts a double precision float value into the dst byte array.
|
||||
func (Encoder) AppendFloat64(dst []byte, val float64, unused int) []byte {
|
||||
switch {
|
||||
case math.IsNaN(val):
|
||||
return append(dst, "\xfb\x7f\xf8\x00\x00\x00\x00\x00\x00"...)
|
||||
case math.IsInf(val, 1):
|
||||
return append(dst, "\xfb\x7f\xf0\x00\x00\x00\x00\x00\x00"...)
|
||||
case math.IsInf(val, -1):
|
||||
return append(dst, "\xfb\xff\xf0\x00\x00\x00\x00\x00\x00"...)
|
||||
}
|
||||
major := majorTypeSimpleAndFloat
|
||||
subType := additionalTypeFloat64
|
||||
n := math.Float64bits(val)
|
||||
dst = append(dst, major|subType)
|
||||
for i := uint(1); i <= 8; i++ {
|
||||
b := byte(n >> ((8 - i) * 8))
|
||||
dst = append(dst, b)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendFloats64 encodes and inserts an array of double precision float values into the dst byte array.
|
||||
func (e Encoder) AppendFloats64(dst []byte, vals []float64, unused int) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(vals)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range vals {
|
||||
dst = e.AppendFloat64(dst, v, unused)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInterface takes an arbitrary object and converts it to JSON and embeds it dst.
|
||||
func (e Encoder) AppendInterface(dst []byte, i interface{}) []byte {
|
||||
marshaled, err := JSONMarshalFunc(i)
|
||||
if err != nil {
|
||||
return e.AppendString(dst, fmt.Sprintf("marshaling error: %v", err))
|
||||
}
|
||||
return AppendEmbeddedJSON(dst, marshaled)
|
||||
}
|
||||
|
||||
// AppendType appends the parameter type (as a string) to the input byte slice.
|
||||
func (e Encoder) AppendType(dst []byte, i interface{}) []byte {
|
||||
if i == nil {
|
||||
return e.AppendString(dst, "<nil>")
|
||||
}
|
||||
return e.AppendString(dst, reflect.TypeOf(i).String())
|
||||
}
|
||||
|
||||
// AppendIPAddr adds a net.IP IPv4 or IPv6 address into the dst byte array.
|
||||
func (e Encoder) AppendIPAddr(dst []byte, ip net.IP) []byte {
|
||||
dst = append(dst, majorTypeTags|additionalTypeIntUint16)
|
||||
dst = append(dst, byte(additionalTypeTagNetworkAddr>>8))
|
||||
dst = append(dst, byte(additionalTypeTagNetworkAddr&0xff))
|
||||
return e.AppendBytes(dst, ip)
|
||||
}
|
||||
|
||||
// AppendIPAddrs adds a []net.IP array of IPv4 or IPv6 address into the dst byte array.
|
||||
func (e Encoder) AppendIPAddrs(dst []byte, ips []net.IP) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(ips)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range ips {
|
||||
dst = e.AppendIPAddr(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendIPPrefix adds a net.IPNet IPv4 or IPv6 Prefix (address & mask) into the dst byte array.
|
||||
func (e Encoder) AppendIPPrefix(dst []byte, pfx net.IPNet) []byte {
|
||||
dst = append(dst, majorTypeTags|additionalTypeIntUint16)
|
||||
dst = append(dst, byte(additionalTypeTagNetworkPrefix>>8))
|
||||
dst = append(dst, byte(additionalTypeTagNetworkPrefix&0xff))
|
||||
|
||||
// Prefix is a tuple (aka MAP of 1 pair of elements) -
|
||||
// first element is prefix, second is mask length.
|
||||
dst = append(dst, majorTypeMap|0x1)
|
||||
dst = e.AppendBytes(dst, pfx.IP)
|
||||
maskLen, _ := pfx.Mask.Size()
|
||||
return e.AppendUint8(dst, uint8(maskLen))
|
||||
}
|
||||
|
||||
// AppendIPPrefixes adds a []net.IPNet array of IPv4 or IPv6 Prefix (address & mask) into the dst byte array.
|
||||
func (e Encoder) AppendIPPrefixes(dst []byte, pfxs []net.IPNet) []byte {
|
||||
major := majorTypeArray
|
||||
l := len(pfxs)
|
||||
if l == 0 {
|
||||
return e.AppendArrayEnd(e.AppendArrayStart(dst))
|
||||
}
|
||||
if l <= additionalMax {
|
||||
lb := byte(l)
|
||||
dst = append(dst, major|lb)
|
||||
} else {
|
||||
dst = appendCborTypePrefix(dst, major, uint64(l))
|
||||
}
|
||||
for _, v := range pfxs {
|
||||
dst = e.AppendIPPrefix(dst, v)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendMACAddr encodes and inserts a Hardware (MAC) address.
|
||||
func (e Encoder) AppendMACAddr(dst []byte, ha net.HardwareAddr) []byte {
|
||||
dst = append(dst, majorTypeTags|additionalTypeIntUint16)
|
||||
dst = append(dst, byte(additionalTypeTagNetworkAddr>>8))
|
||||
dst = append(dst, byte(additionalTypeTagNetworkAddr&0xff))
|
||||
return e.AppendBytes(dst, ha)
|
||||
}
|
||||
|
||||
// AppendHex adds a TAG and inserts a hex bytes as a string.
|
||||
func (e Encoder) AppendHex(dst []byte, val []byte) []byte {
|
||||
dst = append(dst, majorTypeTags|additionalTypeIntUint16)
|
||||
dst = append(dst, byte(additionalTypeTagHexString>>8))
|
||||
dst = append(dst, byte(additionalTypeTagHexString&0xff))
|
||||
return e.AppendBytes(dst, val)
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
package json
|
||||
|
||||
// JSONMarshalFunc is used to marshal interface to JSON encoded byte slice.
|
||||
// Making it package level instead of embedded in Encoder brings
|
||||
// some extra efforts at importing, but avoids value copy when the functions
|
||||
// of Encoder being invoked.
|
||||
// DO REMEMBER to set this variable at importing, or
|
||||
// you might get a nil pointer dereference panic at runtime.
|
||||
var JSONMarshalFunc func(v interface{}) ([]byte, error)
|
||||
|
||||
type Encoder struct{}
|
||||
|
||||
// AppendKey appends a new key to the output JSON.
|
||||
func (e Encoder) AppendKey(dst []byte, key string) []byte {
|
||||
if dst[len(dst)-1] != '{' {
|
||||
dst = append(dst, ',')
|
||||
}
|
||||
return append(e.AppendString(dst, key), ':')
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
package json
|
||||
|
||||
import "unicode/utf8"
|
||||
|
||||
// AppendBytes is a mirror of appendString with []byte arg
|
||||
func (Encoder) AppendBytes(dst, s []byte) []byte {
|
||||
dst = append(dst, '"')
|
||||
for i := 0; i < len(s); i++ {
|
||||
if !noEscapeTable[s[i]] {
|
||||
dst = appendBytesComplex(dst, s, i)
|
||||
return append(dst, '"')
|
||||
}
|
||||
}
|
||||
dst = append(dst, s...)
|
||||
return append(dst, '"')
|
||||
}
|
||||
|
||||
// AppendHex encodes the input bytes to a hex string and appends
|
||||
// the encoded string to the input byte slice.
|
||||
//
|
||||
// The operation loops though each byte and encodes it as hex using
|
||||
// the hex lookup table.
|
||||
func (Encoder) AppendHex(dst, s []byte) []byte {
|
||||
dst = append(dst, '"')
|
||||
for _, v := range s {
|
||||
dst = append(dst, hexCharacters[v>>4], hexCharacters[v&0x0f])
|
||||
}
|
||||
return append(dst, '"')
|
||||
}
|
||||
|
||||
// appendBytesComplex is a mirror of the appendStringComplex
|
||||
// with []byte arg
|
||||
func appendBytesComplex(dst, s []byte, i int) []byte {
|
||||
start := 0
|
||||
for i < len(s) {
|
||||
b := s[i]
|
||||
if b >= utf8.RuneSelf {
|
||||
r, size := utf8.DecodeRune(s[i:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
if start < i {
|
||||
dst = append(dst, s[start:i]...)
|
||||
}
|
||||
dst = append(dst, `\ufffd`...)
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
i += size
|
||||
continue
|
||||
}
|
||||
if noEscapeTable[b] {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
// We encountered a character that needs to be encoded.
|
||||
// Let's append the previous simple characters to the byte slice
|
||||
// and switch our operation to read and encode the remainder
|
||||
// characters byte-by-byte.
|
||||
if start < i {
|
||||
dst = append(dst, s[start:i]...)
|
||||
}
|
||||
switch b {
|
||||
case '"', '\\':
|
||||
dst = append(dst, '\\', b)
|
||||
case '\b':
|
||||
dst = append(dst, '\\', 'b')
|
||||
case '\f':
|
||||
dst = append(dst, '\\', 'f')
|
||||
case '\n':
|
||||
dst = append(dst, '\\', 'n')
|
||||
case '\r':
|
||||
dst = append(dst, '\\', 'r')
|
||||
case '\t':
|
||||
dst = append(dst, '\\', 't')
|
||||
default:
|
||||
dst = append(dst, '\\', 'u', '0', '0', hexCharacters[b>>4], hexCharacters[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
}
|
||||
if start < len(s) {
|
||||
dst = append(dst, s[start:]...)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
const hexCharacters = "0123456789abcdef"
|
||||
|
||||
var noEscapeTable = [256]bool{}
|
||||
|
||||
func init() {
|
||||
for i := 0; i <= 0x7e; i++ {
|
||||
noEscapeTable[i] = i >= 0x20 && i != '\\' && i != '"'
|
||||
}
|
||||
}
|
||||
|
||||
// AppendStrings encodes the input strings to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (e Encoder) AppendStrings(dst []byte, vals []string) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = e.AppendString(dst, vals[0])
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = e.AppendString(append(dst, ','), val)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendString encodes the input string to json and appends
|
||||
// the encoded string to the input byte slice.
|
||||
//
|
||||
// The operation loops though each byte in the string looking
|
||||
// for characters that need json or utf8 encoding. If the string
|
||||
// does not need encoding, then the string is appended in its
|
||||
// entirety to the byte slice.
|
||||
// If we encounter a byte that does need encoding, switch up
|
||||
// the operation and perform a byte-by-byte read-encode-append.
|
||||
func (Encoder) AppendString(dst []byte, s string) []byte {
|
||||
// Start with a double quote.
|
||||
dst = append(dst, '"')
|
||||
// Loop through each character in the string.
|
||||
for i := 0; i < len(s); i++ {
|
||||
// Check if the character needs encoding. Control characters, slashes,
|
||||
// and the double quote need json encoding. Bytes above the ascii
|
||||
// boundary needs utf8 encoding.
|
||||
if !noEscapeTable[s[i]] {
|
||||
// We encountered a character that needs to be encoded. Switch
|
||||
// to complex version of the algorithm.
|
||||
dst = appendStringComplex(dst, s, i)
|
||||
return append(dst, '"')
|
||||
}
|
||||
}
|
||||
// The string has no need for encoding and therefore is directly
|
||||
// appended to the byte slice.
|
||||
dst = append(dst, s...)
|
||||
// End with a double quote
|
||||
return append(dst, '"')
|
||||
}
|
||||
|
||||
// AppendStringers encodes the provided Stringer list to json and
|
||||
// appends the encoded Stringer list to the input byte slice.
|
||||
func (e Encoder) AppendStringers(dst []byte, vals []fmt.Stringer) []byte {
|
||||
if vals == nil || len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = e.AppendStringer(dst, vals[0])
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = e.AppendStringer(append(dst, ','), val)
|
||||
}
|
||||
}
|
||||
return append(dst, ']')
|
||||
}
|
||||
|
||||
// AppendStringer encodes the input Stringer to json and appends the
|
||||
// encoded Stringer value to the input byte slice.
|
||||
func (e Encoder) AppendStringer(dst []byte, val fmt.Stringer) []byte {
|
||||
if val == nil {
|
||||
return e.AppendInterface(dst, nil)
|
||||
}
|
||||
return e.AppendString(dst, val.String())
|
||||
}
|
||||
|
||||
// appendStringComplex is used by appendString to take over an in
|
||||
// progress JSON string encoding that encountered a character that needs
|
||||
// to be encoded.
|
||||
func appendStringComplex(dst []byte, s string, i int) []byte {
|
||||
start := 0
|
||||
for i < len(s) {
|
||||
b := s[i]
|
||||
if b >= utf8.RuneSelf {
|
||||
r, size := utf8.DecodeRuneInString(s[i:])
|
||||
if r == utf8.RuneError && size == 1 {
|
||||
// In case of error, first append previous simple characters to
|
||||
// the byte slice if any and append a replacement character code
|
||||
// in place of the invalid sequence.
|
||||
if start < i {
|
||||
dst = append(dst, s[start:i]...)
|
||||
}
|
||||
dst = append(dst, `\ufffd`...)
|
||||
i += size
|
||||
start = i
|
||||
continue
|
||||
}
|
||||
i += size
|
||||
continue
|
||||
}
|
||||
if noEscapeTable[b] {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
// We encountered a character that needs to be encoded.
|
||||
// Let's append the previous simple characters to the byte slice
|
||||
// and switch our operation to read and encode the remainder
|
||||
// characters byte-by-byte.
|
||||
if start < i {
|
||||
dst = append(dst, s[start:i]...)
|
||||
}
|
||||
switch b {
|
||||
case '"', '\\':
|
||||
dst = append(dst, '\\', b)
|
||||
case '\b':
|
||||
dst = append(dst, '\\', 'b')
|
||||
case '\f':
|
||||
dst = append(dst, '\\', 'f')
|
||||
case '\n':
|
||||
dst = append(dst, '\\', 'n')
|
||||
case '\r':
|
||||
dst = append(dst, '\\', 'r')
|
||||
case '\t':
|
||||
dst = append(dst, '\\', 't')
|
||||
default:
|
||||
dst = append(dst, '\\', 'u', '0', '0', hexCharacters[b>>4], hexCharacters[b&0xF])
|
||||
}
|
||||
i++
|
||||
start = i
|
||||
}
|
||||
if start < len(s) {
|
||||
dst = append(dst, s[start:]...)
|
||||
}
|
||||
return dst
|
||||
}
|
||||
+124
@@ -0,0 +1,124 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// Import from zerolog/global.go
|
||||
timeFormatUnix = ""
|
||||
timeFormatUnixMs = "UNIXMS"
|
||||
timeFormatUnixMicro = "UNIXMICRO"
|
||||
timeFormatUnixNano = "UNIXNANO"
|
||||
durationFormatFloat = "float"
|
||||
durationFormatInt = "int"
|
||||
durationFormatString = "string"
|
||||
)
|
||||
|
||||
// AppendTime formats the input time with the given format
|
||||
// and appends the encoded string to the input byte slice.
|
||||
func (e Encoder) AppendTime(dst []byte, t time.Time, format string) []byte {
|
||||
switch format {
|
||||
case timeFormatUnix:
|
||||
return e.AppendInt64(dst, t.Unix())
|
||||
case timeFormatUnixMs:
|
||||
return e.AppendInt64(dst, t.UnixNano()/1000000)
|
||||
case timeFormatUnixMicro:
|
||||
return e.AppendInt64(dst, t.UnixNano()/1000)
|
||||
case timeFormatUnixNano:
|
||||
return e.AppendInt64(dst, t.UnixNano())
|
||||
}
|
||||
return append(t.AppendFormat(append(dst, '"'), format), '"')
|
||||
}
|
||||
|
||||
// AppendTimes converts the input times with the given format
|
||||
// and appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendTimes(dst []byte, vals []time.Time, format string) []byte {
|
||||
switch format {
|
||||
case timeFormatUnix:
|
||||
return appendUnixTimes(dst, vals)
|
||||
case timeFormatUnixMs:
|
||||
return appendUnixNanoTimes(dst, vals, 1000000)
|
||||
case timeFormatUnixMicro:
|
||||
return appendUnixNanoTimes(dst, vals, 1000)
|
||||
case timeFormatUnixNano:
|
||||
return appendUnixNanoTimes(dst, vals, 1)
|
||||
}
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = append(vals[0].AppendFormat(append(dst, '"'), format), '"')
|
||||
if len(vals) > 1 {
|
||||
for _, t := range vals[1:] {
|
||||
dst = append(t.AppendFormat(append(dst, ',', '"'), format), '"')
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendUnixTimes(dst []byte, vals []time.Time) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = strconv.AppendInt(dst, vals[0].Unix(), 10)
|
||||
if len(vals) > 1 {
|
||||
for _, t := range vals[1:] {
|
||||
dst = strconv.AppendInt(append(dst, ','), t.Unix(), 10)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendUnixNanoTimes(dst []byte, vals []time.Time, div int64) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = strconv.AppendInt(dst, vals[0].UnixNano()/div, 10)
|
||||
if len(vals) > 1 {
|
||||
for _, t := range vals[1:] {
|
||||
dst = strconv.AppendInt(append(dst, ','), t.UnixNano()/div, 10)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendDuration formats the input duration with the given unit & format
|
||||
// and appends the encoded string to the input byte slice.
|
||||
func (e Encoder) AppendDuration(dst []byte, d time.Duration, unit time.Duration, format string, useInt bool, precision int) []byte {
|
||||
if useInt {
|
||||
return strconv.AppendInt(dst, int64(d/unit), 10)
|
||||
}
|
||||
switch format {
|
||||
case durationFormatFloat:
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit), precision)
|
||||
case durationFormatInt:
|
||||
return e.AppendInt64(dst, int64(d/unit))
|
||||
case durationFormatString:
|
||||
return e.AppendString(dst, d.String())
|
||||
}
|
||||
return e.AppendFloat64(dst, float64(d)/float64(unit), precision)
|
||||
}
|
||||
|
||||
// AppendDurations formats the input durations with the given unit & format
|
||||
// and appends the encoded string list to the input byte slice.
|
||||
func (e Encoder) AppendDurations(dst []byte, vals []time.Duration, unit time.Duration, format string, useInt bool, precision int) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = e.AppendDuration(dst, vals[0], unit, format, useInt, precision)
|
||||
if len(vals) > 1 {
|
||||
for _, d := range vals[1:] {
|
||||
dst = e.AppendDuration(append(dst, ','), d, unit, format, useInt, precision)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
+466
@@ -0,0 +1,466 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"net"
|
||||
"reflect"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// AppendNil inserts a 'Nil' object into the dst byte array.
|
||||
func (Encoder) AppendNil(dst []byte) []byte {
|
||||
return append(dst, "null"...)
|
||||
}
|
||||
|
||||
// AppendBeginMarker inserts a map start into the dst byte array.
|
||||
func (Encoder) AppendBeginMarker(dst []byte) []byte {
|
||||
return append(dst, '{')
|
||||
}
|
||||
|
||||
// AppendEndMarker inserts a map end into the dst byte array.
|
||||
func (Encoder) AppendEndMarker(dst []byte) []byte {
|
||||
return append(dst, '}')
|
||||
}
|
||||
|
||||
// AppendLineBreak appends a line break.
|
||||
func (Encoder) AppendLineBreak(dst []byte) []byte {
|
||||
return append(dst, '\n')
|
||||
}
|
||||
|
||||
// AppendArrayStart adds markers to indicate the start of an array.
|
||||
func (Encoder) AppendArrayStart(dst []byte) []byte {
|
||||
return append(dst, '[')
|
||||
}
|
||||
|
||||
// AppendArrayEnd adds markers to indicate the end of an array.
|
||||
func (Encoder) AppendArrayEnd(dst []byte) []byte {
|
||||
return append(dst, ']')
|
||||
}
|
||||
|
||||
// AppendArrayDelim adds markers to indicate end of a particular array element.
|
||||
func (Encoder) AppendArrayDelim(dst []byte) []byte {
|
||||
if len(dst) > 0 {
|
||||
return append(dst, ',')
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendBool converts the input bool to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendBool(dst []byte, val bool) []byte {
|
||||
return strconv.AppendBool(dst, val)
|
||||
}
|
||||
|
||||
// AppendBools encodes the input bools to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendBools(dst []byte, vals []bool) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = strconv.AppendBool(dst, vals[0])
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = strconv.AppendBool(append(dst, ','), val)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInt converts the input int to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendInt(dst []byte, val int) []byte {
|
||||
return strconv.AppendInt(dst, int64(val), 10)
|
||||
}
|
||||
|
||||
// AppendInts encodes the input ints to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendInts(dst []byte, vals []int) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = strconv.AppendInt(dst, int64(vals[0]), 10)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = strconv.AppendInt(append(dst, ','), int64(val), 10)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInt8 converts the input []int8 to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendInt8(dst []byte, val int8) []byte {
|
||||
return strconv.AppendInt(dst, int64(val), 10)
|
||||
}
|
||||
|
||||
// AppendInts8 encodes the input int8s to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendInts8(dst []byte, vals []int8) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = strconv.AppendInt(dst, int64(vals[0]), 10)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = strconv.AppendInt(append(dst, ','), int64(val), 10)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInt16 converts the input int16 to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendInt16(dst []byte, val int16) []byte {
|
||||
return strconv.AppendInt(dst, int64(val), 10)
|
||||
}
|
||||
|
||||
// AppendInts16 encodes the input int16s to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendInts16(dst []byte, vals []int16) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = strconv.AppendInt(dst, int64(vals[0]), 10)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = strconv.AppendInt(append(dst, ','), int64(val), 10)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInt32 converts the input int32 to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendInt32(dst []byte, val int32) []byte {
|
||||
return strconv.AppendInt(dst, int64(val), 10)
|
||||
}
|
||||
|
||||
// AppendInts32 encodes the input int32s to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendInts32(dst []byte, vals []int32) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = strconv.AppendInt(dst, int64(vals[0]), 10)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = strconv.AppendInt(append(dst, ','), int64(val), 10)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInt64 converts the input int64 to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendInt64(dst []byte, val int64) []byte {
|
||||
return strconv.AppendInt(dst, val, 10)
|
||||
}
|
||||
|
||||
// AppendInts64 encodes the input int64s to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendInts64(dst []byte, vals []int64) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = strconv.AppendInt(dst, vals[0], 10)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = strconv.AppendInt(append(dst, ','), val, 10)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendUint converts the input uint to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendUint(dst []byte, val uint) []byte {
|
||||
return strconv.AppendUint(dst, uint64(val), 10)
|
||||
}
|
||||
|
||||
// AppendUints encodes the input uints to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendUints(dst []byte, vals []uint) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = strconv.AppendUint(dst, uint64(vals[0]), 10)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = strconv.AppendUint(append(dst, ','), uint64(val), 10)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendUint8 converts the input uint8 to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendUint8(dst []byte, val uint8) []byte {
|
||||
return strconv.AppendUint(dst, uint64(val), 10)
|
||||
}
|
||||
|
||||
// AppendUints8 encodes the input uint8s to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendUints8(dst []byte, vals []uint8) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = strconv.AppendUint(dst, uint64(vals[0]), 10)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = strconv.AppendUint(append(dst, ','), uint64(val), 10)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendUint16 converts the input uint16 to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendUint16(dst []byte, val uint16) []byte {
|
||||
return strconv.AppendUint(dst, uint64(val), 10)
|
||||
}
|
||||
|
||||
// AppendUints16 encodes the input uint16s to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendUints16(dst []byte, vals []uint16) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = strconv.AppendUint(dst, uint64(vals[0]), 10)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = strconv.AppendUint(append(dst, ','), uint64(val), 10)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendUint32 converts the input uint32 to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendUint32(dst []byte, val uint32) []byte {
|
||||
return strconv.AppendUint(dst, uint64(val), 10)
|
||||
}
|
||||
|
||||
// AppendUints32 encodes the input uint32s to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendUints32(dst []byte, vals []uint32) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = strconv.AppendUint(dst, uint64(vals[0]), 10)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = strconv.AppendUint(append(dst, ','), uint64(val), 10)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendUint64 converts the input uint64 to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendUint64(dst []byte, val uint64) []byte {
|
||||
return strconv.AppendUint(dst, val, 10)
|
||||
}
|
||||
|
||||
// AppendUints64 encodes the input uint64s to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendUints64(dst []byte, vals []uint64) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = strconv.AppendUint(dst, vals[0], 10)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = strconv.AppendUint(append(dst, ','), val, 10)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
func appendFloat(dst []byte, val float64, bitSize, precision int) []byte {
|
||||
// JSON does not permit NaN or Infinity. A typical JSON encoder would fail
|
||||
// with an error, but a logging library wants the data to get through so we
|
||||
// make a tradeoff and store those types as string.
|
||||
switch {
|
||||
case math.IsNaN(val):
|
||||
return append(dst, `"NaN"`...)
|
||||
case math.IsInf(val, 1):
|
||||
return append(dst, `"+Inf"`...)
|
||||
case math.IsInf(val, -1):
|
||||
return append(dst, `"-Inf"`...)
|
||||
}
|
||||
// convert as if by es6 number to string conversion
|
||||
// see also https://cs.opensource.google/go/go/+/refs/tags/go1.20.3:src/encoding/json/encode.go;l=573
|
||||
strFmt := byte('f')
|
||||
// If precision is set to a value other than -1, we always just format the float using that precision.
|
||||
if precision == -1 {
|
||||
// Use float32 comparisons for underlying float32 value to get precise cutoffs right.
|
||||
if abs := math.Abs(val); abs != 0 {
|
||||
if bitSize == 64 && (abs < 1e-6 || abs >= 1e21) || bitSize == 32 && (float32(abs) < 1e-6 || float32(abs) >= 1e21) {
|
||||
strFmt = 'e'
|
||||
}
|
||||
}
|
||||
}
|
||||
dst = strconv.AppendFloat(dst, val, strFmt, precision, bitSize)
|
||||
if strFmt == 'e' {
|
||||
// Clean up e-09 to e-9
|
||||
n := len(dst)
|
||||
if n >= 4 && dst[n-4] == 'e' && dst[n-3] == '-' && dst[n-2] == '0' {
|
||||
dst[n-2] = dst[n-1]
|
||||
dst = dst[:n-1]
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendFloat32 converts the input float32 to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendFloat32(dst []byte, val float32, precision int) []byte {
|
||||
return appendFloat(dst, float64(val), 32, precision)
|
||||
}
|
||||
|
||||
// AppendFloats32 encodes the input float32s to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendFloats32(dst []byte, vals []float32, precision int) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = appendFloat(dst, float64(vals[0]), 32, precision)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = appendFloat(append(dst, ','), float64(val), 32, precision)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendFloat64 converts the input float64 to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (Encoder) AppendFloat64(dst []byte, val float64, precision int) []byte {
|
||||
return appendFloat(dst, val, 64, precision)
|
||||
}
|
||||
|
||||
// AppendFloats64 encodes the input float64s to json and
|
||||
// appends the encoded string list to the input byte slice.
|
||||
func (Encoder) AppendFloats64(dst []byte, vals []float64, precision int) []byte {
|
||||
if len(vals) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = appendFloat(dst, vals[0], 64, precision)
|
||||
if len(vals) > 1 {
|
||||
for _, val := range vals[1:] {
|
||||
dst = appendFloat(append(dst, ','), val, 64, precision)
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendInterface marshals the input interface to a string and
|
||||
// appends the encoded string to the input byte slice.
|
||||
func (e Encoder) AppendInterface(dst []byte, i interface{}) []byte {
|
||||
marshaled, err := JSONMarshalFunc(i)
|
||||
if err != nil {
|
||||
return e.AppendString(dst, fmt.Sprintf("marshaling error: %v", err))
|
||||
}
|
||||
return append(dst, marshaled...)
|
||||
}
|
||||
|
||||
// AppendType appends the parameter type (as a string) to the input byte slice.
|
||||
func (e Encoder) AppendType(dst []byte, i interface{}) []byte {
|
||||
if i == nil {
|
||||
return e.AppendString(dst, "<nil>")
|
||||
}
|
||||
return e.AppendString(dst, reflect.TypeOf(i).String())
|
||||
}
|
||||
|
||||
// AppendObjectData takes in an object that is already in a byte array
|
||||
// and adds it to the dst.
|
||||
func (Encoder) AppendObjectData(dst []byte, o []byte) []byte {
|
||||
// Three conditions apply here:
|
||||
// 1. new content starts with '{' - which should be dropped OR
|
||||
// 2. new content starts with '{' - which should be replaced with ','
|
||||
// to separate with existing content OR
|
||||
// 3. existing content has already other fields
|
||||
if o[0] == '{' {
|
||||
if len(dst) > 1 {
|
||||
dst = append(dst, ',')
|
||||
}
|
||||
o = o[1:]
|
||||
} else if len(dst) > 1 {
|
||||
dst = append(dst, ',')
|
||||
}
|
||||
return append(dst, o...)
|
||||
}
|
||||
|
||||
// AppendIPAddr adds a net.IP IPv4 or IPv6 address to dst.
|
||||
func (e Encoder) AppendIPAddr(dst []byte, ip net.IP) []byte {
|
||||
return e.AppendString(dst, ip.String())
|
||||
}
|
||||
|
||||
// AppendIPAddrs adds a []net.IP array of IPv4 or IPv6 address to dst.
|
||||
func (e Encoder) AppendIPAddrs(dst []byte, ips []net.IP) []byte {
|
||||
if len(ips) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = e.AppendString(dst, ips[0].String())
|
||||
if len(ips) > 1 {
|
||||
for _, ip := range ips[1:] {
|
||||
dst = e.AppendString(append(dst, ','), ip.String())
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendIPPrefix adds a net.IPNet IPv4 or IPv6 Prefix (address & mask) to dst.
|
||||
func (e Encoder) AppendIPPrefix(dst []byte, pfx net.IPNet) []byte {
|
||||
return e.AppendString(dst, pfx.String())
|
||||
}
|
||||
|
||||
// AppendIPPrefixes adds a []net.IPNet array of IPv4 or IPv6 Prefix (address & mask) to dst.
|
||||
func (e Encoder) AppendIPPrefixes(dst []byte, pfxs []net.IPNet) []byte {
|
||||
if len(pfxs) == 0 {
|
||||
return append(dst, '[', ']')
|
||||
}
|
||||
dst = append(dst, '[')
|
||||
dst = e.AppendString(dst, pfxs[0].String())
|
||||
if len(pfxs) > 1 {
|
||||
for _, pfx := range pfxs[1:] {
|
||||
dst = e.AppendString(append(dst, ','), pfx.String())
|
||||
}
|
||||
}
|
||||
dst = append(dst, ']')
|
||||
return dst
|
||||
}
|
||||
|
||||
// AppendMACAddr adds a net.HardwareAddr MAC address to dst.
|
||||
func (e Encoder) AppendMACAddr(dst []byte, ha net.HardwareAddr) []byte {
|
||||
return e.AppendString(dst, ha.String())
|
||||
}
|
||||
+528
@@ -0,0 +1,528 @@
|
||||
// Package zerolog provides a lightweight logging library dedicated to JSON logging.
|
||||
//
|
||||
// A global Logger can be use for simple logging:
|
||||
//
|
||||
// import "github.com/rs/zerolog/log"
|
||||
//
|
||||
// log.Info().Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world"}
|
||||
//
|
||||
// NOTE: To import the global logger, import the "log" subpackage "github.com/rs/zerolog/log".
|
||||
//
|
||||
// Fields can be added to log messages:
|
||||
//
|
||||
// log.Info().Str("foo", "bar").Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
|
||||
//
|
||||
// Create logger instance to manage different outputs:
|
||||
//
|
||||
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
// logger.Info().
|
||||
// Str("foo", "bar").
|
||||
// Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","foo":"bar"}
|
||||
//
|
||||
// Sub-loggers let you chain loggers with additional context:
|
||||
//
|
||||
// sublogger := log.With().Str("component", "foo").Logger()
|
||||
// sublogger.Info().Msg("hello world")
|
||||
// // Output: {"time":1494567715,"level":"info","message":"hello world","component":"foo"}
|
||||
//
|
||||
// Level logging
|
||||
//
|
||||
// zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
//
|
||||
// log.Debug().Msg("filtered out message")
|
||||
// log.Info().Msg("routed message")
|
||||
//
|
||||
// if e := log.Debug(); e.Enabled() {
|
||||
// // Compute log output only if enabled.
|
||||
// value := compute()
|
||||
// e.Str("foo": value).Msg("some debug message")
|
||||
// }
|
||||
// // Output: {"level":"info","time":1494567715,"routed message"}
|
||||
//
|
||||
// Customize automatic field names:
|
||||
//
|
||||
// log.TimestampFieldName = "t"
|
||||
// log.LevelFieldName = "p"
|
||||
// log.MessageFieldName = "m"
|
||||
//
|
||||
// log.Info().Msg("hello world")
|
||||
// // Output: {"t":1494567715,"p":"info","m":"hello world"}
|
||||
//
|
||||
// Log with no level and message:
|
||||
//
|
||||
// log.Log().Str("foo","bar").Msg("")
|
||||
// // Output: {"time":1494567715,"foo":"bar"}
|
||||
//
|
||||
// Add contextual fields to global Logger:
|
||||
//
|
||||
// log.Logger = log.With().Str("foo", "bar").Logger()
|
||||
//
|
||||
// Sample logs:
|
||||
//
|
||||
// sampled := log.Sample(&zerolog.BasicSampler{N: 10})
|
||||
// sampled.Info().Msg("will be logged every 10 messages")
|
||||
//
|
||||
// Log with contextual hooks:
|
||||
//
|
||||
// // Create the hook:
|
||||
// type SeverityHook struct{}
|
||||
//
|
||||
// func (h SeverityHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
|
||||
// if level != zerolog.NoLevel {
|
||||
// e.Str("severity", level.String())
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// // And use it:
|
||||
// var h SeverityHook
|
||||
// log := zerolog.New(os.Stdout).Hook(h)
|
||||
// log.Warn().Msg("")
|
||||
// // Output: {"level":"warn","severity":"warn"}
|
||||
//
|
||||
// # Caveats
|
||||
//
|
||||
// Field duplication:
|
||||
//
|
||||
// There is no fields deduplication out-of-the-box.
|
||||
// Using the same key multiple times creates new key in final JSON each time.
|
||||
//
|
||||
// logger := zerolog.New(os.Stderr).With().Timestamp().Logger()
|
||||
// logger.Info().
|
||||
// Timestamp().
|
||||
// Msg("dup")
|
||||
// // Output: {"level":"info","time":1494567715,"time":1494567715,"message":"dup"}
|
||||
//
|
||||
// In this case, many consumers will take the last value,
|
||||
// but this is not guaranteed; check yours if in doubt.
|
||||
//
|
||||
// Concurrency safety:
|
||||
//
|
||||
// Be careful when calling UpdateContext. It is not concurrency safe. Use the With method to create a child logger:
|
||||
//
|
||||
// func handler(w http.ResponseWriter, r *http.Request) {
|
||||
// // Create a child logger for concurrency safety
|
||||
// logger := log.Logger.With().Logger()
|
||||
//
|
||||
// // Add context fields, for example User-Agent from HTTP headers
|
||||
// logger.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
// ...
|
||||
// })
|
||||
// }
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Level defines log levels.
|
||||
type Level int8
|
||||
|
||||
const (
|
||||
// DebugLevel defines debug log level.
|
||||
DebugLevel Level = iota
|
||||
// InfoLevel defines info log level.
|
||||
InfoLevel
|
||||
// WarnLevel defines warn log level.
|
||||
WarnLevel
|
||||
// ErrorLevel defines error log level.
|
||||
ErrorLevel
|
||||
// FatalLevel defines fatal log level.
|
||||
FatalLevel
|
||||
// PanicLevel defines panic log level.
|
||||
PanicLevel
|
||||
// NoLevel defines an absent log level.
|
||||
NoLevel
|
||||
// Disabled disables the logger.
|
||||
Disabled
|
||||
|
||||
// TraceLevel defines trace log level.
|
||||
TraceLevel Level = -1
|
||||
// Values less than TraceLevel are handled as numbers.
|
||||
)
|
||||
|
||||
func (l Level) String() string {
|
||||
switch l {
|
||||
case TraceLevel:
|
||||
return LevelTraceValue
|
||||
case DebugLevel:
|
||||
return LevelDebugValue
|
||||
case InfoLevel:
|
||||
return LevelInfoValue
|
||||
case WarnLevel:
|
||||
return LevelWarnValue
|
||||
case ErrorLevel:
|
||||
return LevelErrorValue
|
||||
case FatalLevel:
|
||||
return LevelFatalValue
|
||||
case PanicLevel:
|
||||
return LevelPanicValue
|
||||
case Disabled:
|
||||
return "disabled"
|
||||
case NoLevel:
|
||||
return ""
|
||||
}
|
||||
return strconv.Itoa(int(l))
|
||||
}
|
||||
|
||||
// ParseLevel converts a level string into a zerolog Level value.
|
||||
// returns an error if the input string does not match known values.
|
||||
func ParseLevel(levelStr string) (Level, error) {
|
||||
switch {
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(TraceLevel)):
|
||||
return TraceLevel, nil
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(DebugLevel)):
|
||||
return DebugLevel, nil
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(InfoLevel)):
|
||||
return InfoLevel, nil
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(WarnLevel)):
|
||||
return WarnLevel, nil
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(ErrorLevel)):
|
||||
return ErrorLevel, nil
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(FatalLevel)):
|
||||
return FatalLevel, nil
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(PanicLevel)):
|
||||
return PanicLevel, nil
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(Disabled)):
|
||||
return Disabled, nil
|
||||
case strings.EqualFold(levelStr, LevelFieldMarshalFunc(NoLevel)):
|
||||
return NoLevel, nil
|
||||
}
|
||||
i, err := strconv.Atoi(levelStr)
|
||||
if err != nil {
|
||||
return NoLevel, fmt.Errorf("Unknown Level String: '%s', defaulting to NoLevel", levelStr)
|
||||
}
|
||||
if i > 127 || i < -128 {
|
||||
return NoLevel, fmt.Errorf("Out-Of-Bounds Level: '%d', defaulting to NoLevel", i)
|
||||
}
|
||||
return Level(i), nil
|
||||
}
|
||||
|
||||
// UnmarshalText implements encoding.TextUnmarshaler to allow for easy reading from toml/yaml/json formats
|
||||
func (l *Level) UnmarshalText(text []byte) error {
|
||||
if l == nil {
|
||||
return errors.New("can't unmarshal a nil *Level")
|
||||
}
|
||||
var err error
|
||||
*l, err = ParseLevel(string(text))
|
||||
return err
|
||||
}
|
||||
|
||||
// MarshalText implements encoding.TextMarshaler to allow for easy writing into toml/yaml/json formats
|
||||
func (l Level) MarshalText() ([]byte, error) {
|
||||
return []byte(LevelFieldMarshalFunc(l)), nil
|
||||
}
|
||||
|
||||
// A Logger represents an active logging object that generates lines
|
||||
// of JSON output to an io.Writer. Each logging operation makes a single
|
||||
// call to the Writer's Write method. There is no guarantee on access
|
||||
// serialization to the Writer. If your Writer is not thread safe,
|
||||
// you may consider a sync wrapper.
|
||||
type Logger struct {
|
||||
w LevelWriter
|
||||
level Level
|
||||
sampler Sampler
|
||||
context []byte
|
||||
hooks []Hook
|
||||
stack bool
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
// New creates a root logger with given output writer. If the output writer implements
|
||||
// the LevelWriter interface, the WriteLevel method will be called instead of the Write
|
||||
// one.
|
||||
//
|
||||
// Each logging operation makes a single call to the Writer's Write method. There is no
|
||||
// guarantee on access serialization to the Writer. If your Writer is not thread safe,
|
||||
// you may consider using sync wrapper.
|
||||
func New(w io.Writer) Logger {
|
||||
if w == nil {
|
||||
w = io.Discard
|
||||
}
|
||||
lw, ok := w.(LevelWriter)
|
||||
if !ok {
|
||||
lw = LevelWriterAdapter{w}
|
||||
}
|
||||
return Logger{w: lw, level: TraceLevel}
|
||||
}
|
||||
|
||||
// Nop returns a disabled logger for which all operation are no-op.
|
||||
func Nop() Logger {
|
||||
return New(nil).Level(Disabled)
|
||||
}
|
||||
|
||||
// Output duplicates the current logger and sets w as its output.
|
||||
func (l Logger) Output(w io.Writer) Logger {
|
||||
l2 := New(w)
|
||||
l2.level = l.level
|
||||
l2.sampler = l.sampler
|
||||
l2.stack = l.stack
|
||||
if len(l.hooks) > 0 {
|
||||
l2.hooks = append(l2.hooks, l.hooks...)
|
||||
}
|
||||
if l.context != nil {
|
||||
l2.context = make([]byte, len(l.context), cap(l.context))
|
||||
copy(l2.context, l.context)
|
||||
}
|
||||
return l2
|
||||
}
|
||||
|
||||
// With creates a child logger with the field added to its context.
|
||||
func (l Logger) With() Context {
|
||||
context := l.context
|
||||
l.context = make([]byte, 0, 500)
|
||||
if context != nil {
|
||||
l.context = append(l.context, context...)
|
||||
} else {
|
||||
// This is needed for AppendKey to not check len of input
|
||||
// thus making it inlinable
|
||||
l.context = enc.AppendBeginMarker(l.context)
|
||||
}
|
||||
return Context{l}
|
||||
}
|
||||
|
||||
// UpdateContext updates the internal logger's context.
|
||||
//
|
||||
// Caution: This method is not concurrency safe.
|
||||
// Use the With method to create a child logger before modifying the context from concurrent goroutines.
|
||||
func (l *Logger) UpdateContext(update func(c Context) Context) {
|
||||
if l.disabled() {
|
||||
return
|
||||
}
|
||||
if cap(l.context) == 0 {
|
||||
l.context = make([]byte, 0, 500)
|
||||
}
|
||||
if len(l.context) == 0 {
|
||||
l.context = enc.AppendBeginMarker(l.context)
|
||||
}
|
||||
c := update(Context{*l})
|
||||
l.context = c.l.context
|
||||
}
|
||||
|
||||
// Level creates a child logger with the minimum accepted level set to level.
|
||||
func (l Logger) Level(lvl Level) Logger {
|
||||
l.level = lvl
|
||||
return l
|
||||
}
|
||||
|
||||
// GetLevel returns the current Level of l.
|
||||
func (l Logger) GetLevel() Level {
|
||||
return l.level
|
||||
}
|
||||
|
||||
// Sample returns a logger with the s sampler.
|
||||
func (l Logger) Sample(s Sampler) Logger {
|
||||
l.sampler = s
|
||||
return l
|
||||
}
|
||||
|
||||
// Hook returns a logger with the h Hook.
|
||||
func (l Logger) Hook(hooks ...Hook) Logger {
|
||||
if len(hooks) == 0 {
|
||||
return l
|
||||
}
|
||||
newHooks := make([]Hook, len(l.hooks), len(l.hooks)+len(hooks))
|
||||
copy(newHooks, l.hooks)
|
||||
l.hooks = append(newHooks, hooks...)
|
||||
return l
|
||||
}
|
||||
|
||||
// Trace starts a new message with trace level.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Trace() *Event {
|
||||
return l.newEvent(TraceLevel, nil)
|
||||
}
|
||||
|
||||
// Debug starts a new message with debug level.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Debug() *Event {
|
||||
return l.newEvent(DebugLevel, nil)
|
||||
}
|
||||
|
||||
// Info starts a new message with info level.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Info() *Event {
|
||||
return l.newEvent(InfoLevel, nil)
|
||||
}
|
||||
|
||||
// Warn starts a new message with warn level.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Warn() *Event {
|
||||
return l.newEvent(WarnLevel, nil)
|
||||
}
|
||||
|
||||
// Error starts a new message with error level.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Error() *Event {
|
||||
return l.newEvent(ErrorLevel, nil)
|
||||
}
|
||||
|
||||
// Err starts a new message with error level with err as a field if not nil or
|
||||
// with info level if err is nil.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Err(err error) *Event {
|
||||
if err != nil {
|
||||
return l.Error().Err(err)
|
||||
}
|
||||
|
||||
return l.Info()
|
||||
}
|
||||
|
||||
// Fatal starts a new message with fatal level. The FatalExitFunc interceptor function
|
||||
// is called by the Msg method, which by default terminates the program immediately
|
||||
// using os.Exit(1), any desired behavior can be implemented by setting FatalExitFunc.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Fatal() *Event {
|
||||
return l.newEvent(FatalLevel, func(msg string) {
|
||||
if closer, ok := l.w.(io.Closer); ok {
|
||||
// Close the writer to flush any buffered message. Otherwise the message
|
||||
// could be lost if FatalExitFunc() terminates the program immediately or
|
||||
// os.Exit(1) is called if not FatalExitFunc isn't set (default).
|
||||
closer.Close()
|
||||
}
|
||||
if FatalExitFunc != nil {
|
||||
FatalExitFunc()
|
||||
} else {
|
||||
os.Exit(1) // untestable: terminates the program, cannot be covered
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Panic starts a new message with panic level. The panic() function
|
||||
// is called by the Msg method, which stops the ordinary flow of a goroutine.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Panic() *Event {
|
||||
return l.newEvent(PanicLevel, func(msg string) { panic(msg) })
|
||||
}
|
||||
|
||||
// WithLevel starts a new message with level. Unlike Fatal and Panic
|
||||
// methods, WithLevel does not terminate the program or stop the ordinary
|
||||
// flow of a goroutine when used with their respective levels.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) WithLevel(level Level) *Event {
|
||||
switch level {
|
||||
case TraceLevel:
|
||||
return l.Trace()
|
||||
case DebugLevel:
|
||||
return l.Debug()
|
||||
case InfoLevel:
|
||||
return l.Info()
|
||||
case WarnLevel:
|
||||
return l.Warn()
|
||||
case ErrorLevel:
|
||||
return l.Error()
|
||||
case FatalLevel:
|
||||
return l.newEvent(FatalLevel, nil)
|
||||
case PanicLevel:
|
||||
return l.newEvent(PanicLevel, nil)
|
||||
case NoLevel:
|
||||
return l.Log()
|
||||
case Disabled:
|
||||
return nil
|
||||
default:
|
||||
return l.newEvent(level, nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Log starts a new message with no level. Setting GlobalLevel to Disabled
|
||||
// will still disable events produced by this method.
|
||||
//
|
||||
// You must call Msg on the returned event in order to send the event.
|
||||
func (l *Logger) Log() *Event {
|
||||
return l.newEvent(NoLevel, nil)
|
||||
}
|
||||
|
||||
// Print sends a log event using debug level and no extra field.
|
||||
// Arguments are handled in the manner of fmt.Print.
|
||||
func (l *Logger) Print(v ...interface{}) {
|
||||
if e := l.Debug(); e.Enabled() {
|
||||
e.CallerSkipFrame(1).Msg(fmt.Sprint(v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Printf sends a log event using debug level and no extra field.
|
||||
// Arguments are handled in the manner of fmt.Printf.
|
||||
func (l *Logger) Printf(format string, v ...interface{}) {
|
||||
if e := l.Debug(); e.Enabled() {
|
||||
e.CallerSkipFrame(1).Msg(fmt.Sprintf(format, v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Println sends a log event using debug level and no extra field.
|
||||
// Arguments are handled in the manner of fmt.Println.
|
||||
func (l *Logger) Println(v ...interface{}) {
|
||||
if e := l.Debug(); e.Enabled() {
|
||||
e.CallerSkipFrame(1).Msg(fmt.Sprintln(v...))
|
||||
}
|
||||
}
|
||||
|
||||
// Write implements the io.Writer interface. This is useful to set as a writer
|
||||
// for the standard library log.
|
||||
func (l Logger) Write(p []byte) (n int, err error) {
|
||||
n = len(p)
|
||||
if n > 0 && p[n-1] == '\n' {
|
||||
// Trim CR added by stdlog.
|
||||
p = p[0 : n-1]
|
||||
}
|
||||
l.Log().CallerSkipFrame(1).Msg(string(p))
|
||||
return
|
||||
}
|
||||
|
||||
func (l *Logger) newEvent(level Level, done func(string)) *Event {
|
||||
enabled := l.should(level)
|
||||
if !enabled {
|
||||
if done != nil {
|
||||
done("")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
e := newEvent(l.w, level, l.stack, l.ctx, l.hooks)
|
||||
e.done = done
|
||||
if level != NoLevel && LevelFieldName != "" {
|
||||
e.Str(LevelFieldName, LevelFieldMarshalFunc(level))
|
||||
}
|
||||
if len(l.context) > 1 {
|
||||
e.buf = enc.AppendObjectData(e.buf, l.context)
|
||||
}
|
||||
return e
|
||||
}
|
||||
|
||||
func (l *Logger) scratchEvent() *Event {
|
||||
return newEvent(LevelWriterAdapter{io.Discard}, DebugLevel, l.stack, l.ctx, l.hooks)
|
||||
}
|
||||
|
||||
// disabled returns true if the logger is a disabled or nop logger.
|
||||
func (l *Logger) disabled() bool {
|
||||
return l.w == nil || l.level == Disabled
|
||||
}
|
||||
|
||||
// should returns true if the log event should be logged.
|
||||
func (l *Logger) should(lvl Level) bool {
|
||||
if l.disabled() {
|
||||
return false
|
||||
}
|
||||
if lvl < l.level || lvl < GlobalLevel() {
|
||||
return false
|
||||
}
|
||||
if l.sampler != nil && !samplingDisabled() {
|
||||
return l.sampler.Sample(lvl)
|
||||
}
|
||||
return true
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// +build !go1.12
|
||||
|
||||
package zerolog
|
||||
|
||||
const contextCallerSkipFrameCount = 3
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 116 KiB |
+137
@@ -0,0 +1,137 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// Often samples log every ~ 10 events.
|
||||
Often = RandomSampler(10)
|
||||
// Sometimes samples log every ~ 100 events.
|
||||
Sometimes = RandomSampler(100)
|
||||
// Rarely samples log every ~ 1000 events.
|
||||
Rarely = RandomSampler(1000)
|
||||
)
|
||||
|
||||
// Sampler defines an interface to a log sampler.
|
||||
type Sampler interface {
|
||||
// Sample returns true if the event should be part of the sample, false if
|
||||
// the event should be dropped.
|
||||
Sample(lvl Level) bool
|
||||
}
|
||||
|
||||
// RandomSampler use a PRNG to randomly sample an event out of N events,
|
||||
// regardless of their level.
|
||||
type RandomSampler uint32
|
||||
|
||||
// Sample implements the Sampler interface.
|
||||
func (s RandomSampler) Sample(lvl Level) bool {
|
||||
if s <= 0 {
|
||||
return false
|
||||
}
|
||||
if rand.Intn(int(s)) != 0 {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// BasicSampler is a sampler that will send every Nth events, regardless of
|
||||
// their level.
|
||||
type BasicSampler struct {
|
||||
N uint32
|
||||
counter uint32
|
||||
}
|
||||
|
||||
// Sample implements the Sampler interface.
|
||||
func (s *BasicSampler) Sample(lvl Level) bool {
|
||||
n := s.N
|
||||
if n == 0 {
|
||||
return false
|
||||
}
|
||||
if n == 1 {
|
||||
return true
|
||||
}
|
||||
c := atomic.AddUint32(&s.counter, 1)
|
||||
return c%n == 1
|
||||
}
|
||||
|
||||
// BurstSampler lets Burst events pass per Period then pass the decision to
|
||||
// NextSampler. If Sampler is not set, all subsequent events are rejected.
|
||||
type BurstSampler struct {
|
||||
// Burst is the maximum number of event per period allowed before calling
|
||||
// NextSampler.
|
||||
Burst uint32
|
||||
// Period defines the burst period. If 0, NextSampler is always called.
|
||||
Period time.Duration
|
||||
// NextSampler is the sampler used after the burst is reached. If nil,
|
||||
// events are always rejected after the burst.
|
||||
NextSampler Sampler
|
||||
|
||||
counter uint32
|
||||
resetAt int64
|
||||
}
|
||||
|
||||
// Sample implements the Sampler interface.
|
||||
func (s *BurstSampler) Sample(lvl Level) bool {
|
||||
if s.Burst > 0 && s.Period > 0 {
|
||||
if s.inc() <= s.Burst {
|
||||
return true
|
||||
}
|
||||
}
|
||||
if s.NextSampler == nil {
|
||||
return false
|
||||
}
|
||||
return s.NextSampler.Sample(lvl)
|
||||
}
|
||||
|
||||
func (s *BurstSampler) inc() uint32 {
|
||||
now := TimestampFunc().UnixNano()
|
||||
resetAt := atomic.LoadInt64(&s.resetAt)
|
||||
var c uint32
|
||||
if now >= resetAt {
|
||||
c = 1
|
||||
atomic.StoreUint32(&s.counter, c)
|
||||
newResetAt := now + s.Period.Nanoseconds()
|
||||
reset := atomic.CompareAndSwapInt64(&s.resetAt, resetAt, newResetAt)
|
||||
if !reset {
|
||||
// Lost the race with another goroutine trying to reset.
|
||||
c = atomic.AddUint32(&s.counter, 1)
|
||||
}
|
||||
} else {
|
||||
c = atomic.AddUint32(&s.counter, 1)
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// LevelSampler applies a different sampler for each level.
|
||||
type LevelSampler struct {
|
||||
TraceSampler, DebugSampler, InfoSampler, WarnSampler, ErrorSampler Sampler
|
||||
}
|
||||
|
||||
func (s LevelSampler) Sample(lvl Level) bool {
|
||||
switch lvl {
|
||||
case TraceLevel:
|
||||
if s.TraceSampler != nil {
|
||||
return s.TraceSampler.Sample(lvl)
|
||||
}
|
||||
case DebugLevel:
|
||||
if s.DebugSampler != nil {
|
||||
return s.DebugSampler.Sample(lvl)
|
||||
}
|
||||
case InfoLevel:
|
||||
if s.InfoSampler != nil {
|
||||
return s.InfoSampler.Sample(lvl)
|
||||
}
|
||||
case WarnLevel:
|
||||
if s.WarnSampler != nil {
|
||||
return s.WarnSampler.Sample(lvl)
|
||||
}
|
||||
case ErrorLevel:
|
||||
if s.ErrorSampler != nil {
|
||||
return s.ErrorSampler.Sample(lvl)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
+247
@@ -0,0 +1,247 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SlogHandler implements the slog.Handler interface using a zerolog.Logger
|
||||
// as the underlying log backend. This allows code that uses the standard
|
||||
// library's slog package to route log output through zerolog.
|
||||
type SlogHandler struct {
|
||||
logger Logger
|
||||
prefix string // group prefix for nested groups
|
||||
attrs []slog.Attr
|
||||
}
|
||||
|
||||
// NewSlogHandler creates a new slog.Handler that writes log records to the
|
||||
// given zerolog.Logger. The handler maps slog levels to zerolog levels and
|
||||
// converts slog attributes to zerolog fields.
|
||||
func NewSlogHandler(logger Logger) *SlogHandler {
|
||||
return &SlogHandler{logger: logger}
|
||||
}
|
||||
|
||||
// Enabled reports whether the handler handles records at the given level.
|
||||
// It mirrors Logger.should's level and writer checks (without sampling).
|
||||
func (h *SlogHandler) Enabled(_ context.Context, level slog.Level) bool {
|
||||
if h.logger.w == nil {
|
||||
return false
|
||||
}
|
||||
zl := slogToZerologLevel(level)
|
||||
if zl < GlobalLevel() {
|
||||
return false
|
||||
}
|
||||
return zl >= h.logger.level
|
||||
}
|
||||
|
||||
// Handle handles the Record. It converts the slog.Record into a zerolog event
|
||||
// and writes it using the underlying zerolog.Logger.
|
||||
func (h *SlogHandler) Handle(ctx context.Context, record slog.Record) error {
|
||||
zlevel := slogToZerologLevel(record.Level)
|
||||
event := h.logger.WithLevel(zlevel)
|
||||
if event == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Propagate slog context to the zerolog event so that hooks
|
||||
// relying on Event.GetCtx() (e.g. tracing) can access it.
|
||||
if ctx != nil {
|
||||
event = event.Ctx(ctx)
|
||||
}
|
||||
|
||||
// Add pre-attached attrs from WithAttrs
|
||||
for _, a := range h.attrs {
|
||||
event = appendSlogAttr(event, a, h.prefix)
|
||||
}
|
||||
|
||||
// Add attrs from the record itself
|
||||
record.Attrs(func(a slog.Attr) bool {
|
||||
event = appendSlogAttr(event, a, h.prefix)
|
||||
return true
|
||||
})
|
||||
|
||||
// Add timestamp from the slog record, but only if the logger doesn't
|
||||
// already have a timestampHook (added via .With().Timestamp()) to
|
||||
// avoid duplicate timestamp keys in the output.
|
||||
if !record.Time.IsZero() && !h.hasTimestampHook() {
|
||||
event.Time(TimestampFieldName, record.Time)
|
||||
}
|
||||
|
||||
event.Msg(record.Message)
|
||||
return nil
|
||||
}
|
||||
|
||||
// hasTimestampHook reports whether the logger has a timestampHook installed,
|
||||
// which would cause duplicate timestamp fields if we also emit record.Time.
|
||||
func (h *SlogHandler) hasTimestampHook() bool {
|
||||
for _, hook := range h.logger.hooks {
|
||||
if _, ok := hook.(timestampHook); ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// WithAttrs returns a new Handler with the given attributes pre-attached.
|
||||
// These attributes will be included in every subsequent log record.
|
||||
func (h *SlogHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
||||
if len(attrs) == 0 {
|
||||
return h
|
||||
}
|
||||
h2 := h.clone()
|
||||
h2.attrs = append(h2.attrs, attrs...)
|
||||
return h2
|
||||
}
|
||||
|
||||
// WithGroup returns a new Handler with the given group name. All subsequent
|
||||
// attributes will be nested under this group name in the output.
|
||||
func (h *SlogHandler) WithGroup(name string) slog.Handler {
|
||||
if name == "" {
|
||||
return h
|
||||
}
|
||||
h2 := h.clone()
|
||||
if h2.prefix != "" {
|
||||
h2.prefix = h2.prefix + "." + name
|
||||
} else {
|
||||
h2.prefix = name
|
||||
}
|
||||
return h2
|
||||
}
|
||||
|
||||
func (h *SlogHandler) clone() *SlogHandler {
|
||||
h2 := &SlogHandler{
|
||||
logger: h.logger,
|
||||
prefix: h.prefix,
|
||||
}
|
||||
if len(h.attrs) > 0 {
|
||||
h2.attrs = make([]slog.Attr, len(h.attrs))
|
||||
copy(h2.attrs, h.attrs)
|
||||
}
|
||||
return h2
|
||||
}
|
||||
|
||||
// slogToZerologLevel maps slog levels to zerolog levels.
|
||||
//
|
||||
// slog levels: Debug=-4, Info=0, Warn=4, Error=8
|
||||
// zerolog levels: Trace=-1, Debug=0, Info=1, Warn=2, Error=3, Fatal=4, Panic=5
|
||||
func slogToZerologLevel(level slog.Level) Level {
|
||||
switch {
|
||||
case level < slog.LevelDebug:
|
||||
return TraceLevel
|
||||
case level < slog.LevelInfo:
|
||||
return DebugLevel
|
||||
case level < slog.LevelWarn:
|
||||
return InfoLevel
|
||||
case level < slog.LevelError:
|
||||
return WarnLevel
|
||||
default:
|
||||
return ErrorLevel
|
||||
}
|
||||
}
|
||||
|
||||
// zerologToSlogLevel maps zerolog levels to slog levels.
|
||||
func zerologToSlogLevel(level Level) slog.Level {
|
||||
switch level {
|
||||
case TraceLevel:
|
||||
return slog.LevelDebug - 4
|
||||
case DebugLevel:
|
||||
return slog.LevelDebug
|
||||
case InfoLevel:
|
||||
return slog.LevelInfo
|
||||
case WarnLevel:
|
||||
return slog.LevelWarn
|
||||
case ErrorLevel:
|
||||
return slog.LevelError
|
||||
case FatalLevel:
|
||||
return slog.LevelError + 4
|
||||
case PanicLevel:
|
||||
return slog.LevelError + 8
|
||||
default:
|
||||
return slog.LevelInfo
|
||||
}
|
||||
}
|
||||
|
||||
// joinPrefix concatenates a prefix and key with a dot separator.
|
||||
// It avoids allocations when either prefix or key is empty.
|
||||
func joinPrefix(prefix, key string) string {
|
||||
if prefix == "" {
|
||||
return key
|
||||
}
|
||||
if key == "" {
|
||||
return prefix
|
||||
}
|
||||
return prefix + "." + key
|
||||
}
|
||||
|
||||
// appendSlogAttr appends a single slog.Attr to the zerolog event, handling
|
||||
// type-specific encoding to avoid reflection where possible.
|
||||
func appendSlogAttr(event *Event, attr slog.Attr, prefix string) *Event {
|
||||
if event == nil {
|
||||
return event
|
||||
}
|
||||
|
||||
// Resolve the attribute to handle LogValuer types.
|
||||
// This handles slog.KindLogValuer implicitly by unwrapping
|
||||
// any values that implement slog.LogValuer to their resolved form.
|
||||
attr.Value = attr.Value.Resolve()
|
||||
|
||||
// For group kinds, handle grouping before key concatenation
|
||||
if attr.Value.Kind() == slog.KindGroup {
|
||||
attrs := attr.Value.Group()
|
||||
if len(attrs) == 0 {
|
||||
return event
|
||||
}
|
||||
groupPrefix := joinPrefix(prefix, attr.Key)
|
||||
for _, ga := range attrs {
|
||||
event = appendSlogAttr(event, ga, groupPrefix)
|
||||
}
|
||||
return event
|
||||
}
|
||||
|
||||
// Skip empty keys for non-group attributes
|
||||
if attr.Key == "" {
|
||||
return event
|
||||
}
|
||||
|
||||
key := joinPrefix(prefix, attr.Key)
|
||||
val := attr.Value
|
||||
|
||||
switch val.Kind() {
|
||||
case slog.KindString:
|
||||
event = event.Str(key, val.String())
|
||||
case slog.KindInt64:
|
||||
event = event.Int64(key, val.Int64())
|
||||
case slog.KindUint64:
|
||||
event = event.Uint64(key, val.Uint64())
|
||||
case slog.KindFloat64:
|
||||
event = event.Float64(key, val.Float64())
|
||||
case slog.KindBool:
|
||||
event = event.Bool(key, val.Bool())
|
||||
case slog.KindDuration:
|
||||
event = event.Dur(key, val.Duration())
|
||||
case slog.KindTime:
|
||||
event = event.Time(key, val.Time())
|
||||
case slog.KindAny:
|
||||
v := val.Any()
|
||||
switch cv := v.(type) {
|
||||
case error:
|
||||
event = event.AnErr(key, cv)
|
||||
case time.Duration:
|
||||
event = event.Dur(key, cv)
|
||||
case time.Time:
|
||||
event = event.Time(key, cv)
|
||||
case []byte:
|
||||
event = event.Bytes(key, cv)
|
||||
default:
|
||||
event = event.Interface(key, v)
|
||||
}
|
||||
default:
|
||||
event = event.Interface(key, val.Any())
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
// Verify at compile time that SlogHandler satisfies the slog.Handler interface.
|
||||
var _ slog.Handler = (*SlogHandler)(nil)
|
||||
+89
@@ -0,0 +1,89 @@
|
||||
// +build !windows
|
||||
// +build !binary_log
|
||||
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// See http://cee.mitre.org/language/1.0-beta1/clt.html#syslog
|
||||
// or https://www.rsyslog.com/json-elasticsearch/
|
||||
const ceePrefix = "@cee:"
|
||||
|
||||
// SyslogWriter is an interface matching a syslog.Writer struct.
|
||||
type SyslogWriter interface {
|
||||
io.Writer
|
||||
Debug(m string) error
|
||||
Info(m string) error
|
||||
Warning(m string) error
|
||||
Err(m string) error
|
||||
Emerg(m string) error
|
||||
Crit(m string) error
|
||||
}
|
||||
|
||||
type syslogWriter struct {
|
||||
w SyslogWriter
|
||||
prefix string
|
||||
}
|
||||
|
||||
// SyslogLevelWriter wraps a SyslogWriter and call the right syslog level
|
||||
// method matching the zerolog level.
|
||||
func SyslogLevelWriter(w SyslogWriter) LevelWriter {
|
||||
return syslogWriter{w, ""}
|
||||
}
|
||||
|
||||
// SyslogCEEWriter wraps a SyslogWriter with a SyslogLevelWriter that adds a
|
||||
// MITRE CEE prefix for JSON syslog entries, compatible with rsyslog
|
||||
// and syslog-ng JSON logging support.
|
||||
// See https://www.rsyslog.com/json-elasticsearch/
|
||||
func SyslogCEEWriter(w SyslogWriter) LevelWriter {
|
||||
return syslogWriter{w, ceePrefix}
|
||||
}
|
||||
|
||||
func (sw syslogWriter) Write(p []byte) (n int, err error) {
|
||||
var pn int
|
||||
if sw.prefix != "" {
|
||||
pn, err = sw.w.Write([]byte(sw.prefix))
|
||||
if err != nil {
|
||||
return pn, err
|
||||
}
|
||||
}
|
||||
n, err = sw.w.Write(p)
|
||||
return pn + n, err
|
||||
}
|
||||
|
||||
// WriteLevel implements LevelWriter interface.
|
||||
func (sw syslogWriter) WriteLevel(level Level, p []byte) (n int, err error) {
|
||||
switch level {
|
||||
case TraceLevel:
|
||||
case DebugLevel:
|
||||
err = sw.w.Debug(sw.prefix + string(p))
|
||||
case InfoLevel:
|
||||
err = sw.w.Info(sw.prefix + string(p))
|
||||
case WarnLevel:
|
||||
err = sw.w.Warning(sw.prefix + string(p))
|
||||
case ErrorLevel:
|
||||
err = sw.w.Err(sw.prefix + string(p))
|
||||
case FatalLevel:
|
||||
err = sw.w.Emerg(sw.prefix + string(p))
|
||||
case PanicLevel:
|
||||
err = sw.w.Crit(sw.prefix + string(p))
|
||||
case NoLevel:
|
||||
err = sw.w.Info(sw.prefix + string(p))
|
||||
default:
|
||||
panic("invalid level")
|
||||
}
|
||||
// Any CEE prefix is not part of the message, so we don't include its length
|
||||
n = len(p)
|
||||
return
|
||||
}
|
||||
|
||||
// Call the underlying writer's Close method if it is an io.Closer. Otherwise
|
||||
// does nothing.
|
||||
func (sw syslogWriter) Close() error {
|
||||
if c, ok := sw.w.(io.Closer); ok {
|
||||
return c.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+355
@@ -0,0 +1,355 @@
|
||||
package zerolog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"path"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// LevelWriter defines as interface a writer may implement in order
|
||||
// to receive level information with payload.
|
||||
type LevelWriter interface {
|
||||
io.Writer
|
||||
WriteLevel(level Level, p []byte) (n int, err error)
|
||||
}
|
||||
|
||||
// LevelWriterAdapter adapts an io.Writer to support the LevelWriter interface.
|
||||
type LevelWriterAdapter struct {
|
||||
io.Writer
|
||||
}
|
||||
|
||||
// WriteLevel simply writes everything to the adapted writer, ignoring the level.
|
||||
func (lw LevelWriterAdapter) WriteLevel(l Level, p []byte) (n int, err error) {
|
||||
return lw.Write(p)
|
||||
}
|
||||
|
||||
// Call the underlying writer's Close method if it is an io.Closer. Otherwise
|
||||
// does nothing.
|
||||
func (lw LevelWriterAdapter) Close() error {
|
||||
if closer, ok := lw.Writer.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type syncWriter struct {
|
||||
mu sync.Mutex
|
||||
lw LevelWriter
|
||||
}
|
||||
|
||||
// SyncWriter wraps w so that each call to Write is synchronized with a mutex.
|
||||
// This syncer can be used to wrap the call to writer's Write method if it is
|
||||
// not thread safe. Note that you do not need this wrapper for os.File Write
|
||||
// operations on POSIX and Windows systems as they are already thread-safe.
|
||||
func SyncWriter(w io.Writer) io.Writer {
|
||||
if lw, ok := w.(LevelWriter); ok {
|
||||
return &syncWriter{lw: lw}
|
||||
}
|
||||
return &syncWriter{lw: LevelWriterAdapter{w}}
|
||||
}
|
||||
|
||||
// Write implements the io.Writer interface.
|
||||
func (s *syncWriter) Write(p []byte) (n int, err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.lw.Write(p)
|
||||
}
|
||||
|
||||
// WriteLevel implements the LevelWriter interface.
|
||||
func (s *syncWriter) WriteLevel(l Level, p []byte) (n int, err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return s.lw.WriteLevel(l, p)
|
||||
}
|
||||
|
||||
func (s *syncWriter) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if closer, ok := s.lw.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type multiLevelWriter struct {
|
||||
writers []LevelWriter
|
||||
}
|
||||
|
||||
func (t multiLevelWriter) Write(p []byte) (n int, err error) {
|
||||
for _, w := range t.writers {
|
||||
if _n, _err := w.Write(p); err == nil {
|
||||
n = _n
|
||||
if _err != nil {
|
||||
err = _err
|
||||
} else if _n != len(p) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (t multiLevelWriter) WriteLevel(l Level, p []byte) (n int, err error) {
|
||||
for _, w := range t.writers {
|
||||
if _n, _err := w.WriteLevel(l, p); err == nil {
|
||||
n = _n
|
||||
if _err != nil {
|
||||
err = _err
|
||||
} else if _n != len(p) {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
}
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// Calls close on all the underlying writers that are io.Closers. If any of the
|
||||
// Close methods return an error, the remainder of the closers are not closed
|
||||
// and the error is returned.
|
||||
func (t multiLevelWriter) Close() error {
|
||||
for _, w := range t.writers {
|
||||
if closer, ok := w.(io.Closer); ok {
|
||||
if err := closer.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MultiLevelWriter creates a writer that duplicates its writes to all the
|
||||
// provided writers, similar to the Unix tee(1) command. If some writers
|
||||
// implement LevelWriter, their WriteLevel method will be used instead of Write.
|
||||
func MultiLevelWriter(writers ...io.Writer) LevelWriter {
|
||||
lwriters := make([]LevelWriter, 0, len(writers))
|
||||
for _, w := range writers {
|
||||
if lw, ok := w.(LevelWriter); ok {
|
||||
lwriters = append(lwriters, lw)
|
||||
} else {
|
||||
lwriters = append(lwriters, LevelWriterAdapter{w})
|
||||
}
|
||||
}
|
||||
return multiLevelWriter{lwriters}
|
||||
}
|
||||
|
||||
// TestingLog is the logging interface of testing.TB.
|
||||
type TestingLog interface {
|
||||
Log(args ...interface{})
|
||||
Logf(format string, args ...interface{})
|
||||
Helper()
|
||||
}
|
||||
|
||||
// TestWriter is a writer that writes to testing.TB.
|
||||
type TestWriter struct {
|
||||
T TestingLog
|
||||
|
||||
// Frame skips caller frames to capture the original file and line numbers.
|
||||
Frame int
|
||||
}
|
||||
|
||||
// NewTestWriter creates a writer that logs to the testing.TB.
|
||||
func NewTestWriter(t TestingLog) TestWriter {
|
||||
return TestWriter{T: t}
|
||||
}
|
||||
|
||||
// Write to testing.TB.
|
||||
func (t TestWriter) Write(p []byte) (n int, err error) {
|
||||
t.T.Helper()
|
||||
|
||||
n = len(p)
|
||||
|
||||
// Strip trailing newline because t.Log always adds one.
|
||||
p = bytes.TrimRight(p, "\n")
|
||||
|
||||
// Try to correct the log file and line number to the caller.
|
||||
if t.Frame > 0 {
|
||||
_, origFile, origLine, _ := runtime.Caller(1)
|
||||
_, frameFile, frameLine, ok := runtime.Caller(1 + t.Frame)
|
||||
if ok {
|
||||
erase := strings.Repeat("\b", len(path.Base(origFile))+len(strconv.Itoa(origLine))+3)
|
||||
t.T.Logf("%s%s:%d: %s", erase, path.Base(frameFile), frameLine, p)
|
||||
return n, err
|
||||
}
|
||||
}
|
||||
t.T.Log(string(p))
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ConsoleTestWriter creates an option that correctly sets the file frame depth for testing.TB log.
|
||||
func ConsoleTestWriter(t TestingLog) func(w *ConsoleWriter) {
|
||||
return func(w *ConsoleWriter) {
|
||||
w.Out = TestWriter{T: t, Frame: 6}
|
||||
}
|
||||
}
|
||||
|
||||
// FilteredLevelWriter writes only logs at Level or above to Writer.
|
||||
//
|
||||
// It should be used only in combination with MultiLevelWriter when you
|
||||
// want to write to multiple destinations at different levels. Otherwise
|
||||
// you should just set the level on the logger and filter events early.
|
||||
// When using MultiLevelWriter then you set the level on the logger to
|
||||
// the lowest of the levels you use for writers.
|
||||
type FilteredLevelWriter struct {
|
||||
Writer LevelWriter
|
||||
Level Level
|
||||
}
|
||||
|
||||
// Write writes to the underlying Writer.
|
||||
func (w *FilteredLevelWriter) Write(p []byte) (int, error) {
|
||||
return w.Writer.Write(p)
|
||||
}
|
||||
|
||||
// WriteLevel calls WriteLevel of the underlying Writer only if the level is equal
|
||||
// or above the Level.
|
||||
func (w *FilteredLevelWriter) WriteLevel(level Level, p []byte) (int, error) {
|
||||
if level >= w.Level {
|
||||
return w.Writer.WriteLevel(level, p)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Call the underlying writer's Close method if it is an io.Closer. Otherwise
|
||||
// does nothing.
|
||||
func (w *FilteredLevelWriter) Close() error {
|
||||
if closer, ok := w.Writer.(io.Closer); ok {
|
||||
return closer.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var triggerWriterPool = &sync.Pool{
|
||||
New: func() interface{} {
|
||||
return bytes.NewBuffer(make([]byte, 0, 1024))
|
||||
},
|
||||
}
|
||||
|
||||
// TriggerLevelWriter buffers log lines at the ConditionalLevel or below
|
||||
// until a trigger level (or higher) line is emitted. Log lines with level
|
||||
// higher than ConditionalLevel are always written out to the destination
|
||||
// writer. If trigger never happens, buffered log lines are never written out.
|
||||
//
|
||||
// It can be used to configure "log level per request".
|
||||
type TriggerLevelWriter struct {
|
||||
// Destination writer. If LevelWriter is provided (usually), its WriteLevel is used
|
||||
// instead of Write.
|
||||
io.Writer
|
||||
|
||||
// ConditionalLevel is the level (and below) at which lines are buffered until
|
||||
// a trigger level (or higher) line is emitted. Usually this is set to DebugLevel.
|
||||
ConditionalLevel Level
|
||||
|
||||
// TriggerLevel is the lowest level that triggers the sending of the conditional
|
||||
// level lines. Usually this is set to ErrorLevel.
|
||||
TriggerLevel Level
|
||||
|
||||
buf *bytes.Buffer
|
||||
triggered bool
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (w *TriggerLevelWriter) WriteLevel(l Level, p []byte) (n int, err error) {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
// At first trigger level or above log line, we flush the buffer and change the
|
||||
// trigger state to triggered.
|
||||
if !w.triggered && l >= w.TriggerLevel {
|
||||
err := w.trigger()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
// Unless triggered, we buffer everything at and below ConditionalLevel.
|
||||
if !w.triggered && l <= w.ConditionalLevel {
|
||||
if w.buf == nil {
|
||||
w.buf = triggerWriterPool.Get().(*bytes.Buffer)
|
||||
}
|
||||
|
||||
// We prefix each log line with a byte with the level.
|
||||
// Hopefully we will never have a level value which equals a newline
|
||||
// (which could interfere with reconstruction of log lines in the trigger method).
|
||||
w.buf.WriteByte(byte(l))
|
||||
w.buf.Write(p)
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// Anything above ConditionalLevel is always passed through.
|
||||
// Once triggered, everything is passed through.
|
||||
if lw, ok := w.Writer.(LevelWriter); ok {
|
||||
return lw.WriteLevel(l, p)
|
||||
}
|
||||
return w.Write(p)
|
||||
}
|
||||
|
||||
// trigger expects lock to be held.
|
||||
func (w *TriggerLevelWriter) trigger() error {
|
||||
if w.triggered {
|
||||
return nil
|
||||
}
|
||||
w.triggered = true
|
||||
|
||||
if w.buf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
p := w.buf.Bytes()
|
||||
for len(p) > 0 {
|
||||
// We do not use bufio.Scanner here because we already have full buffer
|
||||
// in the memory and we do not want extra copying from the buffer to
|
||||
// scanner's token slice, nor we want to hit scanner's token size limit,
|
||||
// and we also want to preserve newlines.
|
||||
i := bytes.IndexByte(p, '\n')
|
||||
line := p[0 : i+1]
|
||||
p = p[i+1:]
|
||||
// We prefixed each log line with a byte with the level.
|
||||
level := Level(line[0])
|
||||
line = line[1:]
|
||||
var err error
|
||||
if lw, ok := w.Writer.(LevelWriter); ok {
|
||||
_, err = lw.WriteLevel(level, line)
|
||||
} else {
|
||||
_, err = w.Write(line)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Trigger forces flushing the buffer and change the trigger state to
|
||||
// triggered, if the writer has not already been triggered before.
|
||||
func (w *TriggerLevelWriter) Trigger() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
return w.trigger()
|
||||
}
|
||||
|
||||
// Close closes the writer and returns the buffer to the pool.
|
||||
func (w *TriggerLevelWriter) Close() error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
|
||||
if w.buf == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// We return the buffer only if it has not grown above the limit.
|
||||
// This prevents accumulation of large buffers in the pool just
|
||||
// because occasionally a large buffer might be needed.
|
||||
if w.buf.Cap() <= TriggerLevelWriterBufferReuseLimit {
|
||||
w.buf.Reset()
|
||||
triggerWriterPool.Put(w.buf)
|
||||
}
|
||||
w.buf = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
module github.com/silenceper/wechat/v2
|
||||
|
||||
go 1.14
|
||||
|
||||
require (
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b
|
||||
github.com/fatih/structs v1.1.0
|
||||
github.com/gomodule/redigo v1.8.5
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
||||
github.com/sirupsen/logrus v1.8.1
|
||||
github.com/spf13/cast v1.3.1
|
||||
github.com/stretchr/testify v1.7.0
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83
|
||||
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
|
||||
gopkg.in/h2non/gock.v1 v1.0.15
|
||||
)
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
|
||||
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
|
||||
github.com/gomodule/redigo v1.8.5 h1:nRAxCa+SVsyjSBrtZmG/cqb6VbTmuRzpg/PoTFlpumc=
|
||||
github.com/gomodule/redigo v1.8.5/go.mod h1:P9dn9mFrCBvWhGE1wpxx6fgq7BAeLBk+UUUzlpkBYO0=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
|
||||
github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
|
||||
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/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
|
||||
github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
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/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng=
|
||||
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 h1:/ZScEX8SfEmUGRHs0gxpqteO5nfNW6axyZbBdw9A12g=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2 h1:46ULzRKLh1CwgRq2dC5SlBzEqqNCi8rreOZnNrbqcIY=
|
||||
golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15 h1:SzLqcIlb/fDfg7UvukMpNcWsu7sI5tWwL+KCATZqks0=
|
||||
gopkg.in/h2non/gock.v1 v1.0.15/go.mod h1:sX4zAkdYX1TRGJ2JY156cFspQn4yRWn6p9EMdODlynE=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
module github.com/sirupsen/logrus
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.2.2
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037
|
||||
)
|
||||
|
||||
go 1.13
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
module github.com/spf13/cast
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/stretchr/testify v1.2.2
|
||||
)
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
module github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common
|
||||
|
||||
go 1.14
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
module github.com/xdg-go/pbkdf2
|
||||
|
||||
go 1.9
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user