You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Zerolog Hook中提取事件字段并集成Azure App Insights

Zerolog与Azure App Insights集成:字段获取、实现优化及API建议

我已经编写了Zerolog与Azure App Insights集成的代码,但卡在了Hook函数中如何获取事件字段,无法将这些数据作为自定义属性发送到App Insights。另外想请教有没有更简洁的实现方式,以及是否有开发者认为Zerolog应该暴露获取字段的公共函数?

package zeroappinsights

import (
    "context"
    "encoding/json"
    "github.com/gin-gonic/gin"
    "net/http"
    "os"
    "time"

    "github.com/microsoft/ApplicationInsights-Go/appinsights"
    "github.com/microsoft/ApplicationInsights-Go/appinsights/contracts"
    "github.com/rs/zerolog"
    "github.com/rs/zerolog/log"

    "github.com/org/package/constants"
    "github.com/org/package/models"
)

var levelMap = map[zerolog.Level]contracts.SeverityLevel{
    zerolog.ErrorLevel: contracts.Error,
    zerolog.InfoLevel:  contracts.Information,
    zerolog.DebugLevel: contracts.Verbose,
    zerolog.FatalLevel: contracts.Critical,
    zerolog.WarnLevel:  contracts.Warning,
}

var appInsightsProperties = []string{
    contracts.OperationId,
    contracts.OperationParentId,
    contracts.UserId,
    contracts.SessionId,
}

type TracingHook struct {
    env             constants.Environment
    telemetryClient appinsights.TelemetryClient
    file            *os.File
    eventsMap       map[string]string
}

func NewAITracing(serviceName, instrumentationKey string, env constants.Environment, eventsMap map[string]string) *TracingHook {
    client := appinsights.NewTelemetryClient(instrumentationKey)
    client.Context().Tags.Cloud().SetRole(serviceName)
    if env.IsLocal() {
        file, err := os.OpenFile(serviceName+".log", os.O_CREATE, 0644)
        if err != nil {
            return nil
        }

        return &TracingHook{
            telemetryClient: client,
            file:            file,
            env:             constants.Local,
        }
    }
    return &TracingHook{
        telemetryClient: client,
        file:            nil,
        env:             env,
        eventsMap:       eventsMap,
    }
}

func (h *TracingHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
    ctx := e.GetCtx()
    if appinsightsLevel, ok := levelMap[level]; ok {
        trace := appinsights.NewTraceTelemetry(msg, appinsightsLevel)
        for _, property := range appInsightsProperties {
            trace.Tags[property] = ctx.Value(property).(string)
        } 
        //I want to add more properties from the event to trace.Properties
        trace.Timestamp = time.Now().UTC()
        if h.env.IsLocal() {
            // Marshal the request into a JSON byte slice
            traceJSON, err := json.Marshal(trace)
            if err != nil {
                log.Printf("error marshaling request: %v", err)
                return
            }
            if _, err = h.file.Write(append(traceJSON, '\n')); err != nil {
                log.Printf("error occurred while writing to file: %v", err)
                return
            }
        } else {
            go h.telemetryClient.Track(trace)
        }
    }

}

func (h *TracingHook) LoggingMiddleware(logger zerolog.Logger) func(c *gin.Context) {
    return func(c *gin.Context) {
        startTime := time.Now().UTC()
        telemetry := appinsights.NewEventTelemetry(h.eventsMap[c.Request.URL.Path])
        h.telemetryClient.Track(telemetry)
        operationID := telemetry.Tags[contracts.OperationId]
        operationParentID := telemetry.Tags[contracts.OperationParentId]
        userID := telemetry.Tags[contracts.UserId]
        sessionID := telemetry.Tags[contracts.SessionId]

        if value, exists := c.Get("userProfile"); exists {
            profile := value.(models.UserProfile)
            userID = profile.ID
            sessionID = c.Request.Header.Get(constants.Session)
        }

        values := []string{operationID, operationParentID, userID, sessionID}

        ctx := c.Request.Context()
        for i, key := range appInsightsProperties {
            ctx = context.WithValue(ctx, key, values[i])
        }
        loggerContext := logger.WithContext(ctx)
        c.Set("loggerCtx", loggerContext)
        c.Next()

        duration := time.Since(startTime)
        request := appinsights.NewRequestTelemetry(c.Request.Method, c.Request.URL.Path, duration, http.StatusText(c.Writer.Status()))
        request.Timestamp = time.Now().UTC()
        tags := request.Tags
        tags[contracts.OperationId] = operationID
        tags[contracts.OperationParentId] = operationParentID
        tags[contracts.UserId] = userID
        tags[contracts.SessionId] = sessionID
        request.Tags = tags

        if h.env.IsLocal() {
            // Marshal the request into a JSON byte slice
            requestJSON, err := json.Marshal(request)
            if err != nil {
                log.Printf("error marshaling request: %v", err)
                return
            }
            if _, err = h.file.Write(append(requestJSON, '\n')); err != nil {
                log.Printf("error occurred while writing to file: %v", err)
                return
            }
            return
        }
        h.telemetryClient.Track(request)
    }
}

一、Hook中获取Zerolog事件字段的方案

Zerolog的Event结构体未直接暴露字段访问API,但可以通过克隆事件并序列化到内存缓冲区的方式解析字段,不影响原日志输出:

// 导入bytes和fmt包
import (
    "bytes"
    "fmt"
)

func (h *TracingHook) Run(e *zerolog.Event, level zerolog.Level, msg string) {
    ctx := e.GetCtx()
    if appinsightsLevel, ok := levelMap[level]; ok {
        trace := appinsights.NewTraceTelemetry(msg, appinsightsLevel)
        for _, property := range appInsightsProperties {
            if val := ctx.Value(property); val != nil {
                trace.Tags[property] = val.(string)
            }
        }

        // 解析事件中的自定义字段
        var buf bytes.Buffer
        // 克隆事件避免干扰原日志流程
        clonedEvent := e.With()
        clonedEvent.Str("msg", msg).Send(&buf)
        
        var eventData map[string]interface{}
        if err := json.Unmarshal(buf.Bytes(), &eventData); err == nil {
            // 过滤默认字段,添加自定义属性到App Insights
            for k, v := range eventData {
                if k != "msg" && k != "level" && k != "time" {
                    trace.Properties[k] = fmt.Sprintf("%v", v)
                }
            }
        }

        trace.Timestamp = time.Now().UTC()
        // 原本地/云端上报逻辑不变
        if h.env.IsLocal() {
            traceJSON, err := json.Marshal(trace)
            if err != nil {
                log.Printf("error marshaling request: %v", err)
                return
            }
            if _, err = h.file.Write(append(traceJSON, '\n')); err != nil {
                log.Printf("error occurred while writing to file: %v", err)
                return
            }
        } else {
            go h.telemetryClient.Track(trace)
        }
    }
}

二、更简洁的实现优化

1. 简化上下文传递逻辑

把OperationId等标签直接存入Gin上下文,避免嵌套context.WithValue:

// 中间件中替换原上下文传递代码
c.Set(contracts.OperationId, operationID)
c.Set(contracts.OperationParentId, operationParentID)
c.Set(contracts.UserId, userID)
c.Set(contracts.SessionId, sessionID)

Hook中直接从Gin上下文获取:

if ginCtx, ok := ctx.(*gin.Context); ok {
    for _, property := range appInsightsProperties {
        if val, exists := ginCtx.Get(property); exists {
            trace.Tags[property] = val.(string)
        }
    }
}

2. 抽取公共序列化逻辑

把本地日志的序列化代码抽成公共方法,避免重复:

func (h *TracingHook) writeLocalLog(telemetry interface{}) {
    data, err := json.Marshal(telemetry)
    if err != nil {
        log.Printf("marshal telemetry failed: %v", err)
        return
    }
    if _, err = h.file.Write(append(data, '\n')); err != nil {
        log.Printf("write local log failed: %v", err)
    }
}

在Run方法和中间件中直接调用:

if h.env.IsLocal() {
    h.writeLocalLog(trace)
} else {
    go h.telemetryClient.Track(trace)
}

3. 懒加载本地日志文件

将文件打开逻辑移到第一次写入时,避免初始化失败导致Hook创建失败:

func (h *TracingHook) getLogFile(serviceName string) *os.File {
    if h.file == nil && h.env.IsLocal() {
        file, err := os.OpenFile(serviceName+".log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
        if err == nil {
            h.file = file
        }
    }
    return h.file
}

三、关于Zerolog暴露字段API的讨论

社区中确实有不少开发者提出过类似需求,但Zerolog的设计核心是高性能(零分配、低开销),直接暴露字段映射可能会破坏其性能特性,因此官方目前未提供相关API。

如果需要更直接的字段访问,可尝试:

  • 在创建日志时手动将字段存入上下文,Hook中直接从上下文读取
  • 向Zerolog仓库提交Issue,说明你的业务场景和需求,推动官方评估添加API的可能性

内容的提问来源于stack exchange,提问作者Hariharan Sivakumar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 02:54:54