feat(logging): 增加 Seq 日志集成与标准输出重定向
- 新增 SeqWriter 支持,将日志异步推送到 Seq 平台,支持多进程实例区分 - 实现 redirectStdout 函数,重定向 os.Stdout 和标准 log 包输出,确保 fmt.Println 等输出被捕获并记录 - 更新 README 文档,增加 Seq 日志集成的说明与配置链接 - 扩展 Logger 结构,支持动态添加输出目标,提升日志记录灵活性
This commit is contained in:
@@ -18,6 +18,7 @@ D:\app\go1.23.1\bin\go.exe test ./dd/... -v
|
||||
- **代码生成** - 根据数据库表自动生成 CRUD 接口
|
||||
- **API 测试** - 内置接口测试框架,链式 API、事务自动回滚、覆盖率报告、交互式调试控制台
|
||||
- **优雅停机** - 跨平台(Windows/Linux)支持,停机时自动拒绝新请求并等待在途请求完成,配合 nginx 实现零停机滚动重启
|
||||
- **结构化日志 / Seq 集成** - zerolog 驱动,异步 channel 队列推送,零阻塞接入 Seq 集中搜索,自动捕获 fmt.Println 等全量输出,支持单机多进程实例区分
|
||||
- **开箱即用** - 微信支付/公众号/小程序、阿里云、腾讯云等 SDK 内置
|
||||
|
||||
## 文档
|
||||
@@ -33,6 +34,7 @@ D:\app\go1.23.1\bin\go.exe test ./dd/... -v
|
||||
| [代码生成配置规范](docs/CodeConfig_代码生成配置规范.md) | codeConfig、菜单权限、字段规则配置说明 |
|
||||
| [API 测试框架](docs/Testing_API测试框架.md) | 接口测试、事务隔离、覆盖率追踪、API 调试控制台生成 |
|
||||
| [优雅停机](docs/graceful-shutdown.md) | 跨平台优雅停机、nginx 配置、滚动重启操作指南 |
|
||||
| [Seq 日志集成](docs/Seq日志集成.md) | Seq 接入配置、异步队列原理、单机多进程实例区分、全量日志捕获、搜索语法速查 |
|
||||
| [改进规划](docs/ROADMAP_改进规划.md) | 待改进项、设计思考、版本迭代规划 |
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package hotime
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"database/sql"
|
||||
"io/ioutil"
|
||||
stdlog "log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -340,6 +343,16 @@ func (that *Application) SetConfig(configPath ...string) {
|
||||
that.WebConnectLog = log.NewLoggerNoCaller(1, that.Config.GetString("webConnectLogFile"), 0)
|
||||
}
|
||||
|
||||
// 重定向 os.Stdout 和标准 log 包,捕获 fmt.Println 等绕过 HoTime Logger 的输出
|
||||
redirectStdout(that.Log)
|
||||
|
||||
// 接入 Seq:instance = ip:port,同机多进程靠端口区分,跨机器靠 IP 区分
|
||||
if seqUrl := that.Config.GetString("seqUrl"); seqUrl != "" {
|
||||
instance := getLocalIP() + ":" + that.Config.GetString("port")
|
||||
that.Log.SetSeqWriter(seqUrl, that.Config.GetString("seqApiKey"), instance)
|
||||
that.Log.Infof("Seq 日志推送已启动: url=%s instance=%s", seqUrl, instance)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// SetConnectListener 连接判断,返回false继续传输至控制层,true则停止传输
|
||||
@@ -623,6 +636,42 @@ func (that *Application) crossDomain(context *Context, sessionId string) {
|
||||
|
||||
}
|
||||
|
||||
// getLocalIP 通过 UDP dial 探测方式获取本机出口 IP(不发送任何数据,无副作用)。
|
||||
// 用于构建 Seq instance 字段,格式为 ip:port,支持跨机器集群识别。
|
||||
// 失败时返回 "unknown"。
|
||||
func getLocalIP() string {
|
||||
conn, err := net.Dial("udp", "8.8.8.8:80")
|
||||
if err != nil {
|
||||
return "unknown"
|
||||
}
|
||||
defer conn.Close()
|
||||
return conn.LocalAddr().(*net.UDPAddr).IP.String()
|
||||
}
|
||||
|
||||
// redirectStdout 重定向 os.Stdout 和标准 log 包到 HoTime Logger。
|
||||
// 捕获 fmt.Println/fmt.Printf 等绕过 HoTime 日志系统的输出,
|
||||
// 以 INFO 级别 + source="stdout" 字段写入,后续经 SeqWriter 推送到 Seq。
|
||||
func redirectStdout(l *log.Logger) {
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
os.Stdout = w
|
||||
stdlog.SetOutput(w)
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line != "" {
|
||||
l.Info().Str("source", "stdout").Msg(line)
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
l.Errorf("[stdout redirect] scanner error: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Init 初始化application
|
||||
func Init(config string) *Application {
|
||||
appIns := Application{}
|
||||
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
# Seq 日志集成
|
||||
|
||||
HoTime 框架内置 Seq 日志推送支持。通过在 `config.json` 填写 `seqUrl` 即可激活,
|
||||
所有框架日志(含 `fmt.Println` 等绕过 Logger 的输出)实时推送到 Seq,
|
||||
通过 Seq Web UI 实现关键词、日期范围、日志级别、结构化字段等多维度搜索。
|
||||
|
||||
---
|
||||
|
||||
## 配置项
|
||||
|
||||
在 `config.json` 中添加:
|
||||
|
||||
```json
|
||||
"seqUrl": "http://127.0.0.1:5341",
|
||||
"seqApiKey": ""
|
||||
```
|
||||
|
||||
| 字段 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `seqUrl` | 空(不激活) | Seq 服务地址,空值时功能静默不生效 |
|
||||
| `seqApiKey` | 空 | API Key,免费单用户版留空 |
|
||||
|
||||
`instance` 字段由框架自动拼接为 `ip:port`(如 `192.168.1.10:8085`),无需手动填写,同机多进程靠端口区分,跨服务器靠 IP 区分,支持未来集群扩展。
|
||||
|
||||
---
|
||||
|
||||
## 架构原理
|
||||
|
||||
```
|
||||
业务代码
|
||||
│ l.Info().Msg("...") ← HoTime Logger 正常调用路径
|
||||
│ fmt.Println("...") ← 被 redirectStdout 捕获后转入同一路径
|
||||
↓
|
||||
multiWriter(hotimev1.5/log/logger.go)
|
||||
├─ ConsoleWriter → 彩色终端输出(不变)
|
||||
├─ FileWriter → 本地日志文件(按需,logFile 配置)
|
||||
└─ SeqWriter
|
||||
│ Write() 只做 channel <- bytes,O(1) 非阻塞
|
||||
↓
|
||||
channel(容量 10000)
|
||||
↓ 后台 goroutine
|
||||
批量打包(100 条 或 500ms)
|
||||
↓ HTTP POST
|
||||
Seq 服务(CLEF 格式)
|
||||
```
|
||||
|
||||
**关键特性:**
|
||||
- `SeqWriter.Write()` 仅向 channel 投递字节即返回,**绝不阻塞** web 请求处理 goroutine
|
||||
- channel 满时(Seq 宕机/网络故障)新日志被丢弃并计数,主服务完全不受影响
|
||||
- HTTP POST 设 5s 超时,失败仅打印到 stderr
|
||||
|
||||
---
|
||||
|
||||
## 字段映射(zerolog → CLEF)
|
||||
|
||||
| zerolog 字段 | Seq CLEF 字段 | 说明 |
|
||||
|---|---|---|
|
||||
| `time` | `@t` | 时间,自动转 ISO 8601 格式 |
|
||||
| `level` | `@l` | 级别,映射为 Debug/Information/Warning/Error/Fatal |
|
||||
| `message` / `msg` | `@mt` | 消息正文 |
|
||||
| `caller` | `caller` | 调用位置,原样保留 |
|
||||
| 其余自定义字段 | 原字段名 | 原样保留,可在 Seq 中直接查询 |
|
||||
| — | `instance` | 框架自动注入,值为 `port` 配置(如 `"8085"`) |
|
||||
| — | `source` | fmt.Println 等捕获的输出标记为 `stdout` |
|
||||
|
||||
---
|
||||
|
||||
## 单机多进程实例区分
|
||||
|
||||
框架启动时自动获取本机出口 IP,拼接为 `ip:port` 格式作为 `instance`:
|
||||
|
||||
```
|
||||
单机多进程:
|
||||
192.168.1.10:8085 ─┐
|
||||
192.168.1.10:8086 ─┼─ HTTP CLEF ──→ Seq
|
||||
192.168.1.10:8087 ─┘
|
||||
|
||||
多服务器集群:
|
||||
192.168.1.10:8085 ─┐
|
||||
192.168.1.11:8085 ─┼─ HTTP CLEF ──→ Seq(中央日志服务器)
|
||||
192.168.1.12:8085 ─┘
|
||||
```
|
||||
|
||||
Seq 中按实例筛选:
|
||||
- 单台机器所有进程:`instance like '192.168.1.10%'`
|
||||
- 精确到某个进程:`instance = '192.168.1.10:8085'`
|
||||
|
||||
---
|
||||
|
||||
## stdout 全量捕获
|
||||
|
||||
框架启动时(`SetConfig()` 中)自动调用 `redirectStdout()`,
|
||||
将 `os.Stdout` 和标准 `log` 包重定向到 HoTime Logger:
|
||||
|
||||
- `fmt.Println("xxx")` → 以 `INFO` 级别、`source=stdout` 字段推送到 Seq
|
||||
- `log.Printf("xxx")` → 同上
|
||||
- 控制台仍然能看到这些输出(输出管道由 goroutine 实时转发)
|
||||
|
||||
---
|
||||
|
||||
## Seq 安装
|
||||
|
||||
Seq 提供 Windows MSI 安装包和 Docker 镜像,单机免费,无外部数据库依赖:
|
||||
|
||||
- Windows:[https://datalust.co/download/seq](https://datalust.co/download/seq),安装后自动注册为 Windows 服务
|
||||
- Docker:`docker run -d --restart always --name seq -p 5341:80 -e ACCEPT_EULA=Y datalust/seq`
|
||||
- 访问 `http://localhost:5341` 使用 Web UI
|
||||
|
||||
---
|
||||
|
||||
## 搜索语法速查
|
||||
|
||||
| 目标 | 查询语句 |
|
||||
|---|---|
|
||||
| 关键词搜索 | 直接输入,如 `支付失败` |
|
||||
| 日志级别 | `@l = 'Error'` |
|
||||
| 特定实例 | `instance = '8085'` |
|
||||
| stdout 来源 | `source = 'stdout'` |
|
||||
| 调用位置 | `caller like '%order.go%'` |
|
||||
| 组合查询 | `@l = 'Error' and instance = '8086' and @mt like '%超时%'` |
|
||||
| 日期范围 | 右上角时间选择器,支持精确到秒 |
|
||||
+214
-5
@@ -2,13 +2,17 @@ package log
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
@@ -25,6 +29,7 @@ type ErrorRecord struct {
|
||||
// Logger 日志核心结构体,封装 zerolog
|
||||
type Logger struct {
|
||||
zl zerolog.Logger
|
||||
mw *multiWriter // 动态多输出,支持运行时追加 SeqWriter 等
|
||||
logLevel int
|
||||
errors []ErrorRecord
|
||||
errIdx int // 环形缓冲写入位置
|
||||
@@ -69,8 +74,8 @@ func NewLogger(logLevel int, logFile string, maxErrors int) *Logger {
|
||||
writers = append(writers, fw)
|
||||
}
|
||||
|
||||
multi := zerolog.MultiLevelWriter(writers...)
|
||||
zl := zerolog.New(multi).
|
||||
mw := &multiWriter{writers: writers}
|
||||
zl := zerolog.New(mw).
|
||||
Level(level).
|
||||
With().
|
||||
Timestamp().
|
||||
@@ -79,6 +84,7 @@ func NewLogger(logLevel int, logFile string, maxErrors int) *Logger {
|
||||
|
||||
l := &Logger{
|
||||
zl: zl,
|
||||
mw: mw,
|
||||
logLevel: logLevel,
|
||||
maxErrors: maxErrors,
|
||||
}
|
||||
@@ -116,8 +122,8 @@ func NewLoggerNoCaller(logLevel int, logFile string, maxErrors int) *Logger {
|
||||
writers = append(writers, fw)
|
||||
}
|
||||
|
||||
multi := zerolog.MultiLevelWriter(writers...)
|
||||
zl := zerolog.New(multi).
|
||||
mw := &multiWriter{writers: writers}
|
||||
zl := zerolog.New(mw).
|
||||
Level(level).
|
||||
With().
|
||||
Timestamp().
|
||||
@@ -125,6 +131,7 @@ func NewLoggerNoCaller(logLevel int, logFile string, maxErrors int) *Logger {
|
||||
|
||||
l := &Logger{
|
||||
zl: zl,
|
||||
mw: mw,
|
||||
logLevel: logLevel,
|
||||
maxErrors: maxErrors,
|
||||
}
|
||||
@@ -145,6 +152,7 @@ func NewTestLogger() *Logger {
|
||||
func (l *Logger) SetOutput(w io.Writer) {
|
||||
if w == io.Discard {
|
||||
l.zl = zerolog.New(w).Level(zerolog.Disabled)
|
||||
l.mw = nil
|
||||
return
|
||||
}
|
||||
consoleWriter := zerolog.ConsoleWriter{
|
||||
@@ -159,7 +167,9 @@ func (l *Logger) SetOutput(w io.Writer) {
|
||||
return fmt.Sprintf("[%s]", i)
|
||||
},
|
||||
}
|
||||
l.zl = zerolog.New(consoleWriter).
|
||||
mw := &multiWriter{writers: []io.Writer{consoleWriter}}
|
||||
l.mw = mw
|
||||
l.zl = zerolog.New(mw).
|
||||
With().
|
||||
Timestamp().
|
||||
Caller().
|
||||
@@ -422,6 +432,205 @@ func shortenPath(file string) string {
|
||||
return file
|
||||
}
|
||||
|
||||
// --- multiWriter: 支持运行时追加输出目标 ---
|
||||
|
||||
// multiWriter 实现 zerolog.LevelWriter,允许在 Logger 创建后动态追加新的输出(如 SeqWriter)
|
||||
type multiWriter struct {
|
||||
mu sync.RWMutex
|
||||
writers []io.Writer
|
||||
}
|
||||
|
||||
func (mw *multiWriter) Write(p []byte) (int, error) {
|
||||
mw.mu.RLock()
|
||||
defer mw.mu.RUnlock()
|
||||
for _, w := range mw.writers {
|
||||
_, _ = w.Write(p)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// WriteLevel 实现 zerolog.LevelWriter,使 zerolog 的级别过滤正确传递给各子 Writer
|
||||
func (mw *multiWriter) WriteLevel(l zerolog.Level, p []byte) (int, error) {
|
||||
mw.mu.RLock()
|
||||
defer mw.mu.RUnlock()
|
||||
for _, w := range mw.writers {
|
||||
if lw, ok := w.(zerolog.LevelWriter); ok {
|
||||
_, _ = lw.WriteLevel(l, p)
|
||||
} else {
|
||||
_, _ = w.Write(p)
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (mw *multiWriter) add(w io.Writer) {
|
||||
mw.mu.Lock()
|
||||
mw.writers = append(mw.writers, w)
|
||||
mw.mu.Unlock()
|
||||
}
|
||||
|
||||
// --- SeqWriter: 异步推送到 Seq(CLEF 格式)---
|
||||
|
||||
// SeqWriter 将 zerolog JSON 日志通过 channel 队列异步发送到 Seq。
|
||||
// Write() 仅做 channel <- bytes,O(1) 非阻塞,绝不阻塞 web 请求处理 goroutine。
|
||||
// channel 容量 10000,满时丢弃新条目(dropped 计数),不会影响主服务。
|
||||
type SeqWriter struct {
|
||||
seqUrl string
|
||||
apiKey string
|
||||
instance string
|
||||
queue chan []byte
|
||||
dropped int64
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newSeqWriter(seqUrl, apiKey, instance string) *SeqWriter {
|
||||
w := &SeqWriter{
|
||||
seqUrl: strings.TrimRight(seqUrl, "/"),
|
||||
apiKey: apiKey,
|
||||
instance: instance,
|
||||
queue: make(chan []byte, 10000),
|
||||
client: &http.Client{Timeout: 5 * time.Second},
|
||||
}
|
||||
go w.run()
|
||||
return w
|
||||
}
|
||||
|
||||
// Write 将字节塞入 channel 即返回,由后台 goroutine 消费发送
|
||||
func (w *SeqWriter) Write(p []byte) (int, error) {
|
||||
clef := w.toClef(p)
|
||||
select {
|
||||
case w.queue <- clef:
|
||||
default:
|
||||
atomic.AddInt64(&w.dropped, 1)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
// toClef 将 zerolog JSON 字段映射到 Seq CLEF 格式
|
||||
// zerolog: time/level/message(msg) → CLEF: @t/@l/@mt
|
||||
func (w *SeqWriter) toClef(p []byte) []byte {
|
||||
trimmed := bytes.TrimSpace(p)
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(trimmed, &m); err != nil {
|
||||
// 非 JSON(如 fmt.Println 直接输出),包装为纯文本日志
|
||||
return []byte(fmt.Sprintf(`{"@t":%q,"@l":"Information","@mt":%q,"source":"stdout","instance":%q}`,
|
||||
time.Now().Format(time.RFC3339), strings.TrimSpace(string(p)), w.instance))
|
||||
}
|
||||
|
||||
clef := make(map[string]interface{}, len(m)+3)
|
||||
for k, v := range m {
|
||||
clef[k] = v
|
||||
}
|
||||
|
||||
// time → @t(zerolog 格式 "2006-01-02 15:04:05" → ISO 8601)
|
||||
if t, ok := m["time"].(string); ok {
|
||||
delete(clef, "time")
|
||||
if pt, err := time.ParseInLocation("2006-01-02 15:04:05", t, time.Local); err == nil {
|
||||
clef["@t"] = pt.Format(time.RFC3339)
|
||||
} else {
|
||||
clef["@t"] = t
|
||||
}
|
||||
} else {
|
||||
clef["@t"] = time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// level → @l
|
||||
if lv, ok := m["level"].(string); ok {
|
||||
delete(clef, "level")
|
||||
switch lv {
|
||||
case "debug":
|
||||
clef["@l"] = "Debug"
|
||||
case "info":
|
||||
clef["@l"] = "Information"
|
||||
case "warn":
|
||||
clef["@l"] = "Warning"
|
||||
case "error":
|
||||
clef["@l"] = "Error"
|
||||
case "fatal":
|
||||
clef["@l"] = "Fatal"
|
||||
default:
|
||||
clef["@l"] = lv
|
||||
}
|
||||
}
|
||||
|
||||
// message/msg → @mt
|
||||
if msg, ok := m["message"].(string); ok {
|
||||
delete(clef, "message")
|
||||
clef["@mt"] = msg
|
||||
} else if msg, ok := m["msg"].(string); ok {
|
||||
delete(clef, "msg")
|
||||
clef["@mt"] = msg
|
||||
}
|
||||
|
||||
if w.instance != "" {
|
||||
clef["instance"] = w.instance
|
||||
}
|
||||
|
||||
b, _ := json.Marshal(clef)
|
||||
return b
|
||||
}
|
||||
|
||||
// run 后台 goroutine:每 100 条或 500ms 批量 POST 到 Seq
|
||||
func (w *SeqWriter) run() {
|
||||
batch := make([][]byte, 0, 100)
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case entry := <-w.queue:
|
||||
batch = append(batch, entry)
|
||||
if len(batch) >= 100 {
|
||||
w.flush(batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
case <-ticker.C:
|
||||
if len(batch) > 0 {
|
||||
w.flush(batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *SeqWriter) flush(batch [][]byte) {
|
||||
var buf bytes.Buffer
|
||||
for _, entry := range batch {
|
||||
buf.Write(entry)
|
||||
buf.WriteByte('\n')
|
||||
}
|
||||
req, err := http.NewRequest("POST", w.seqUrl+"/api/events/raw?clef", &buf)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[seq] build request error: %v\n", err)
|
||||
return
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/vnd.serilog.clef")
|
||||
if w.apiKey != "" {
|
||||
req.Header.Set("X-Seq-ApiKey", w.apiKey)
|
||||
}
|
||||
resp, err := w.client.Do(req)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[seq] send error: %v (dropped=%d)\n", err, atomic.LoadInt64(&w.dropped))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode >= 400 {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
fmt.Fprintf(os.Stderr, "[seq] server returned %d: %s\n", resp.StatusCode, string(body))
|
||||
}
|
||||
}
|
||||
|
||||
// SetSeqWriter 将 Seq HTTP 推送器附加到当前 Logger(不影响已有的控制台/文件输出)
|
||||
// seqUrl: Seq 服务地址,如 "http://127.0.0.1:5341"
|
||||
// apiKey: Seq API Key,免费单用户版留空
|
||||
// instance: 实例标识,建议用 config 的 port 字段区分同机多进程,如 "8085"
|
||||
func (l *Logger) SetSeqWriter(seqUrl, apiKey, instance string) {
|
||||
if seqUrl == "" || l.mw == nil {
|
||||
return
|
||||
}
|
||||
sw := newSeqWriter(seqUrl, apiKey, instance)
|
||||
l.mw.add(sw)
|
||||
}
|
||||
|
||||
// isInfrastructureFile 判断文件是否为基础设施(日志/DB/缓存/通用工具)管线,
|
||||
// 这些文件在调用栈中会被跳过,以显示真正的业务调用者。
|
||||
// context.go 是 Display/View 的纯中间层,也作为基础设施跳过。
|
||||
|
||||
@@ -61,6 +61,8 @@ var ConfigNote = Map{
|
||||
"logHistory": "默认100,非必须,内存中保留最近N条错误日志,用于调试调阅,通过 Log.GetRecentErrors() 获取",
|
||||
"webConnectLogShow": "默认true,非必须,访问日志如果需要web访问链接、访问ip、访问时间打印,false为关闭true开启此功能",
|
||||
"webConnectLogFile": "无默认,非必须,webConnectLogShow开启之后才能使用,如果需要存储日志文件时使用,保存格式为:a/b/c/20060102150405.txt,将生成:a/b/c/年月日时分秒.txt,按需设置",
|
||||
"seqUrl": "无默认,非必须,Seq 日志平台地址,如 http://127.0.0.1:5341,填写后自动将所有日志通过异步 channel 队列推送到 Seq,空值则不激活;同时自动将 fmt.Println/标准log 包的输出也一并捕获推送;instance 字段自动拼接为 ip:port(如 192.168.1.10:8085),支持单机多进程和多服务器集群",
|
||||
"seqApiKey": "无默认,非必须,Seq API Key,单机免费版留空即可,多用户或有认证要求时填写",
|
||||
//"codeConfig": Map{
|
||||
// "注释": "配置即启用,非必须,默认无",
|
||||
// //"package":"默认admin,必须,mode模式为2时会自动生成包文件夹和代码文件",
|
||||
|
||||
Reference in New Issue
Block a user