mirror of
https://github.com/EZ-Api/ez-api.git
synced 2026-01-13 17:47:51 +00:00
feat(telemetry): implement request tracing identifier
Introduce a middleware layer to attach a unique identifier to each HTTP request for observability purposes. The identifier is propagated via the X-Request-ID header, allowing for correlation of logs and events across distributed system components.
This commit is contained in:
18
README.md
18
README.md
@@ -76,12 +76,13 @@ docker run -p 8080:8080 --env-file .env ez-api
|
|||||||
|
|
||||||
### 本地联合开发(配合 balancer)
|
### 本地联合开发(配合 balancer)
|
||||||
|
|
||||||
- 目录结构建议:`/workspace/` 下并列放置 `ez-api` 与 `balancer`。
|
- 目录结构建议:`/workspace/` 下并列放置 `ez-api`、`balancer`、`foundation`。
|
||||||
- 初始化/更新 Go 工作区(Go 1.20+):在 `/workspace` 执行
|
- 初始化本地 Go 工作区(Go 1.20+,不要求提交到任一仓库):在 `/workspace` 执行
|
||||||
```bash
|
```bash
|
||||||
go work use ./ez-api ./ez-api/test ./balancer
|
go work init
|
||||||
|
go work use ./ez-api ./balancer ./foundation
|
||||||
```
|
```
|
||||||
仓库已附带 `go.work`,若路径一致可直接复用;若放在其他位置请按上面命令重建。
|
如果你只使用已发布的 `github.com/ez-api/foundation vX.Y.Z`,则不需要 `go.work`。
|
||||||
|
|
||||||
### 集成测试
|
### 集成测试
|
||||||
|
|
||||||
@@ -97,12 +98,17 @@ cd ez-api
|
|||||||
## 日志
|
## 日志
|
||||||
|
|
||||||
- 业务代码统一使用标准库 `log/slog`(`logger.Info("msg", "k", v)` 风格)。
|
- 业务代码统一使用标准库 `log/slog`(`logger.Info("msg", "k", v)` 风格)。
|
||||||
- 输出后端仍为 [zerolog](https://github.com/rs/zerolog)(通过 `internal/logging` 的 slog handler bridge),默认 ConsoleWriter。
|
- 输出后端仍为 [zerolog](https://github.com/rs/zerolog)(通过 `github.com/ez-api/foundation/logging` 的 slog handler bridge),默认 ConsoleWriter。
|
||||||
- 通过 `EZ_LOG_LEVEL` 控制控制平面的日志等级,配合异步 DB 写入(LogWriter)一起使用。
|
- 通过 `EZ_LOG_LEVEL` 控制控制平面的日志等级,配合异步 DB 写入(LogWriter)一起使用。
|
||||||
|
|
||||||
## JSON
|
## JSON
|
||||||
|
|
||||||
- 项目内 JSON 编解码统一走 `ez-api/internal/jsoncodec`(内部使用 Sonic)。
|
- 项目内 JSON 编解码统一走 `github.com/ez-api/foundation/jsoncodec`(内部使用 Sonic)。
|
||||||
|
|
||||||
|
## 依赖约定
|
||||||
|
|
||||||
|
- Control Plane(ez-api)与 Data Plane(balancer)共享一部分“协议约定/基础设施”代码(JSON、日志、provider type 规则),统一沉淀在 `github.com/ez-api/foundation`。
|
||||||
|
- 本仓库的 `go.mod` 需要依赖一个可用的 `foundation` 版本:发布后建议锁定到 `vX.Y.Z`;本地联调可使用 `go.work`(见下文)。
|
||||||
|
|
||||||
## 设计决策
|
## 设计决策
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ import (
|
|||||||
_ "github.com/ez-api/ez-api/docs"
|
_ "github.com/ez-api/ez-api/docs"
|
||||||
"github.com/ez-api/ez-api/internal/api"
|
"github.com/ez-api/ez-api/internal/api"
|
||||||
"github.com/ez-api/ez-api/internal/config"
|
"github.com/ez-api/ez-api/internal/config"
|
||||||
"github.com/ez-api/ez-api/internal/logging"
|
|
||||||
"github.com/ez-api/ez-api/internal/middleware"
|
"github.com/ez-api/ez-api/internal/middleware"
|
||||||
"github.com/ez-api/ez-api/internal/model"
|
"github.com/ez-api/ez-api/internal/model"
|
||||||
"github.com/ez-api/ez-api/internal/service"
|
"github.com/ez-api/ez-api/internal/service"
|
||||||
|
"github.com/ez-api/foundation/logging"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
swaggerFiles "github.com/swaggo/files"
|
swaggerFiles "github.com/swaggo/files"
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -4,6 +4,7 @@ go 1.24.5
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/bytedance/sonic v1.14.0
|
github.com/bytedance/sonic v1.14.0
|
||||||
|
github.com/ez-api/foundation v0.0.0
|
||||||
github.com/gin-gonic/gin v1.11.0
|
github.com/gin-gonic/gin v1.11.0
|
||||||
github.com/redis/go-redis/v9 v9.17.2
|
github.com/redis/go-redis/v9 v9.17.2
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/ez-api/ez-api/internal/dto"
|
"github.com/ez-api/ez-api/internal/dto"
|
||||||
"github.com/ez-api/ez-api/internal/model"
|
"github.com/ez-api/ez-api/internal/model"
|
||||||
"github.com/ez-api/ez-api/internal/service"
|
"github.com/ez-api/ez-api/internal/service"
|
||||||
|
"github.com/ez-api/foundation/provider"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
@@ -43,6 +44,9 @@ func (h *Handler) CreateProvider(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
providerType := provider.NormalizeType(req.Type)
|
||||||
|
googleLocation := provider.DefaultGoogleLocation(providerType, req.GoogleLocation)
|
||||||
|
|
||||||
group := strings.TrimSpace(req.Group)
|
group := strings.TrimSpace(req.Group)
|
||||||
if group == "" {
|
if group == "" {
|
||||||
group = "default"
|
group = "default"
|
||||||
@@ -59,11 +63,11 @@ func (h *Handler) CreateProvider(c *gin.Context) {
|
|||||||
|
|
||||||
provider := model.Provider{
|
provider := model.Provider{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Type: req.Type,
|
Type: strings.TrimSpace(req.Type),
|
||||||
BaseURL: req.BaseURL,
|
BaseURL: req.BaseURL,
|
||||||
APIKey: req.APIKey,
|
APIKey: req.APIKey,
|
||||||
GoogleProject: strings.TrimSpace(req.GoogleProject),
|
GoogleProject: strings.TrimSpace(req.GoogleProject),
|
||||||
GoogleLocation: strings.TrimSpace(req.GoogleLocation),
|
GoogleLocation: googleLocation,
|
||||||
Group: group,
|
Group: group,
|
||||||
Models: strings.Join(req.Models, ","),
|
Models: strings.Join(req.Models, ","),
|
||||||
Status: status,
|
Status: status,
|
||||||
@@ -122,12 +126,18 @@ func (h *Handler) UpdateProvider(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
nextType := strings.TrimSpace(existing.Type)
|
||||||
|
if t := strings.TrimSpace(req.Type); t != "" {
|
||||||
|
nextType = t
|
||||||
|
}
|
||||||
|
nextTypeLower := provider.NormalizeType(nextType)
|
||||||
|
|
||||||
update := map[string]any{}
|
update := map[string]any{}
|
||||||
if strings.TrimSpace(req.Name) != "" {
|
if strings.TrimSpace(req.Name) != "" {
|
||||||
update["name"] = req.Name
|
update["name"] = req.Name
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(req.Type) != "" {
|
if strings.TrimSpace(req.Type) != "" {
|
||||||
update["type"] = req.Type
|
update["type"] = strings.TrimSpace(req.Type)
|
||||||
}
|
}
|
||||||
if strings.TrimSpace(req.BaseURL) != "" {
|
if strings.TrimSpace(req.BaseURL) != "" {
|
||||||
update["base_url"] = req.BaseURL
|
update["base_url"] = req.BaseURL
|
||||||
@@ -140,6 +150,8 @@ func (h *Handler) UpdateProvider(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if strings.TrimSpace(req.GoogleLocation) != "" {
|
if strings.TrimSpace(req.GoogleLocation) != "" {
|
||||||
update["google_location"] = strings.TrimSpace(req.GoogleLocation)
|
update["google_location"] = strings.TrimSpace(req.GoogleLocation)
|
||||||
|
} else if provider.IsVertexFamily(nextTypeLower) && strings.TrimSpace(existing.GoogleLocation) == "" {
|
||||||
|
update["google_location"] = provider.DefaultGoogleLocation(nextTypeLower, "")
|
||||||
}
|
}
|
||||||
if req.Models != nil {
|
if req.Models != nil {
|
||||||
update["models"] = strings.Join(req.Models, ",")
|
update["models"] = strings.Join(req.Models, ",")
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
package jsoncodec
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/bytedance/sonic"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Marshal(v any) ([]byte, error) { return sonic.Marshal(v) }
|
|
||||||
|
|
||||||
func Unmarshal(data []byte, v any) error { return sonic.Unmarshal(data, v) }
|
|
||||||
|
|
||||||
func UnmarshalString(data string, v any) error { return sonic.UnmarshalString(data, v) }
|
|
||||||
|
|
||||||
func NewEncoder(w io.Writer) sonic.Encoder { return sonic.ConfigDefault.NewEncoder(w) }
|
|
||||||
|
|
||||||
func NewDecoder(r io.Reader) sonic.Decoder { return sonic.ConfigDefault.NewDecoder(r) }
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package logging
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Options struct {
|
|
||||||
Service string
|
|
||||||
}
|
|
||||||
|
|
||||||
func New(opts Options) (*slog.Logger, zerolog.Logger) {
|
|
||||||
level := parseLevel(strings.TrimSpace(os.Getenv("EZ_LOG_LEVEL")))
|
|
||||||
zerolog.SetGlobalLevel(toZerologLevel(level))
|
|
||||||
|
|
||||||
output := zerolog.ConsoleWriter{
|
|
||||||
Out: os.Stdout,
|
|
||||||
TimeFormat: time.RFC3339,
|
|
||||||
}
|
|
||||||
|
|
||||||
zl := zerolog.New(output).
|
|
||||||
Level(toZerologLevel(level)).
|
|
||||||
With().
|
|
||||||
Timestamp().
|
|
||||||
Str("service", strings.TrimSpace(opts.Service)).
|
|
||||||
Logger()
|
|
||||||
|
|
||||||
sl := slog.New(NewZerologHandler(zl, level))
|
|
||||||
slog.SetDefault(sl)
|
|
||||||
return sl, zl
|
|
||||||
}
|
|
||||||
|
|
||||||
func parseLevel(raw string) slog.Level {
|
|
||||||
switch strings.ToLower(raw) {
|
|
||||||
case "debug":
|
|
||||||
return slog.LevelDebug
|
|
||||||
case "warn", "warning":
|
|
||||||
return slog.LevelWarn
|
|
||||||
case "error":
|
|
||||||
return slog.LevelError
|
|
||||||
case "info", "":
|
|
||||||
return slog.LevelInfo
|
|
||||||
default:
|
|
||||||
return slog.LevelInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toZerologLevel(level slog.Level) zerolog.Level {
|
|
||||||
switch {
|
|
||||||
case level <= slog.LevelDebug:
|
|
||||||
return zerolog.DebugLevel
|
|
||||||
case level <= slog.LevelInfo:
|
|
||||||
return zerolog.InfoLevel
|
|
||||||
case level <= slog.LevelWarn:
|
|
||||||
return zerolog.WarnLevel
|
|
||||||
default:
|
|
||||||
return zerolog.ErrorLevel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,133 +0,0 @@
|
|||||||
package logging
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"log/slog"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ZerologHandler struct {
|
|
||||||
logger zerolog.Logger
|
|
||||||
level slog.Level
|
|
||||||
attrs []slog.Attr
|
|
||||||
groups []string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewZerologHandler(logger zerolog.Logger, level slog.Level) *ZerologHandler {
|
|
||||||
return &ZerologHandler{
|
|
||||||
logger: logger,
|
|
||||||
level: level,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ZerologHandler) Enabled(_ context.Context, level slog.Level) bool {
|
|
||||||
return level >= h.level
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ZerologHandler) Handle(_ context.Context, record slog.Record) error {
|
|
||||||
event := h.eventFor(record.Level)
|
|
||||||
if event == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, attr := range h.attrs {
|
|
||||||
h.addAttr(event, h.key(attr.Key), attr.Value)
|
|
||||||
}
|
|
||||||
record.Attrs(func(attr slog.Attr) bool {
|
|
||||||
h.addAttr(event, h.key(attr.Key), attr.Value)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
|
|
||||||
event.Msg(record.Message)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ZerologHandler) WithAttrs(attrs []slog.Attr) slog.Handler {
|
|
||||||
cp := h.clone()
|
|
||||||
cp.attrs = append(cp.attrs, attrs...)
|
|
||||||
return cp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ZerologHandler) WithGroup(name string) slog.Handler {
|
|
||||||
if strings.TrimSpace(name) == "" {
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
cp := h.clone()
|
|
||||||
cp.groups = append(cp.groups, name)
|
|
||||||
return cp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ZerologHandler) clone() *ZerologHandler {
|
|
||||||
cp := *h
|
|
||||||
cp.attrs = append([]slog.Attr(nil), h.attrs...)
|
|
||||||
cp.groups = append([]string(nil), h.groups...)
|
|
||||||
return &cp
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ZerologHandler) key(k string) string {
|
|
||||||
k = strings.TrimSpace(k)
|
|
||||||
if k == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if len(h.groups) == 0 {
|
|
||||||
return k
|
|
||||||
}
|
|
||||||
return strings.Join(h.groups, ".") + "." + k
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ZerologHandler) eventFor(level slog.Level) *zerolog.Event {
|
|
||||||
switch {
|
|
||||||
case level >= slog.LevelError:
|
|
||||||
return h.logger.Error()
|
|
||||||
case level >= slog.LevelWarn:
|
|
||||||
return h.logger.Warn()
|
|
||||||
case level >= slog.LevelInfo:
|
|
||||||
return h.logger.Info()
|
|
||||||
default:
|
|
||||||
return h.logger.Debug()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *ZerologHandler) addAttr(event *zerolog.Event, key string, value slog.Value) {
|
|
||||||
if event == nil || strings.TrimSpace(key) == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
value = value.Resolve()
|
|
||||||
|
|
||||||
switch value.Kind() {
|
|
||||||
case slog.KindGroup:
|
|
||||||
for _, groupAttr := range value.Group() {
|
|
||||||
groupKey := h.key(key + "." + groupAttr.Key)
|
|
||||||
h.addAttr(event, groupKey, groupAttr.Value.Resolve())
|
|
||||||
}
|
|
||||||
case slog.KindString:
|
|
||||||
event.Str(key, value.String())
|
|
||||||
case slog.KindBool:
|
|
||||||
event.Bool(key, value.Bool())
|
|
||||||
case slog.KindInt64:
|
|
||||||
event.Int64(key, value.Int64())
|
|
||||||
case slog.KindUint64:
|
|
||||||
event.Uint64(key, value.Uint64())
|
|
||||||
case slog.KindFloat64:
|
|
||||||
event.Float64(key, value.Float64())
|
|
||||||
case slog.KindDuration:
|
|
||||||
event.Dur(key, value.Duration())
|
|
||||||
case slog.KindTime:
|
|
||||||
event.Time(key, value.Time())
|
|
||||||
default:
|
|
||||||
anyValue := value.Any()
|
|
||||||
if err, ok := anyValue.(error); ok {
|
|
||||||
event.AnErr(key, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if stringer, ok := anyValue.(fmt.Stringer); ok {
|
|
||||||
event.Str(key, stringer.String())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
event.Interface(key, anyValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/ez-api/ez-api/internal/jsoncodec"
|
|
||||||
"github.com/ez-api/ez-api/internal/model"
|
"github.com/ez-api/ez-api/internal/model"
|
||||||
"github.com/ez-api/ez-api/internal/util"
|
"github.com/ez-api/ez-api/internal/util"
|
||||||
|
"github.com/ez-api/foundation/jsoncodec"
|
||||||
"github.com/redis/go-redis/v9"
|
"github.com/redis/go-redis/v9"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user