modelRT/middleware/trace.go

164 lines
5.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Package middleware defines gin framework middlewares and OTel tracing infrastructure.
package middleware
import (
"bytes"
"context"
"fmt"
"io"
"strings"
"time"
"modelRT/config"
"modelRT/constants"
"modelRT/logger"
"github.com/gin-gonic/gin"
"go.opentelemetry.io/contrib/propagators/b3"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
"go.opentelemetry.io/otel/propagation"
sdkresource "go.opentelemetry.io/otel/sdk/resource"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
oteltrace "go.opentelemetry.io/otel/trace"
)
// InitTracerProvider creates an OTLP TracerProvider and registers it as the global provider.
// It also registers the B3 propagator to stay compatible with existing B3 infrastructure.
// The caller is responsible for calling Shutdown on the returned provider during graceful shutdown.
func InitTracerProvider(ctx context.Context, cfg config.ModelRTConfig) (*sdktrace.TracerProvider, error) {
opts := []otlptracehttp.Option{
otlptracehttp.WithEndpoint(cfg.OtelConfig.Endpoint),
}
if cfg.OtelConfig.Insecure {
opts = append(opts, otlptracehttp.WithInsecure())
}
exporter, err := otlptracehttp.New(ctx, opts...)
if err != nil {
return nil, fmt.Errorf("create OTLP exporter: %w", err)
}
res := sdkresource.NewSchemaless(
attribute.String("service.name", cfg.ServiceName),
attribute.String("deployment.environment", cfg.DeployEnv),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exporter),
sdktrace.WithResource(res),
sdktrace.WithSampler(sdktrace.AlwaysSample()),
)
otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(b3.New())
return tp, nil
}
// StartTrace extracts upstream B3 trace context from request headers and starts a server span.
// Typed context keys are also injected for backward compatibility with the existing logger
// until the logger is migrated to read from the OTel span context (Step 6).
func StartTrace() gin.HandlerFunc {
tracer := otel.Tracer("modelRT/http")
return func(c *gin.Context) {
// Extract upstream trace context from B3 headers (X-B3-TraceId etc.)
ctx := otel.GetTextMapPropagator().Extract(
c.Request.Context(),
propagation.HeaderCarrier(c.Request.Header),
)
spanName := c.FullPath()
if spanName == "" {
spanName = c.Request.URL.Path
}
ctx, span := tracer.Start(ctx, spanName,
oteltrace.WithSpanKind(oteltrace.SpanKindServer),
)
defer span.End()
// backward compat: inject typed keys so existing logger reads work until Step 6
spanCtx := span.SpanContext()
ctx = context.WithValue(ctx, constants.CtxKeyTraceID, spanCtx.TraceID().String())
ctx = context.WithValue(ctx, constants.CtxKeySpanID, spanCtx.SpanID().String())
c.Request = c.Request.WithContext(ctx)
// set in gin context for accessLog (logger.New(c) reads via gin.Context.Value)
c.Set(constants.HeaderTraceID, spanCtx.TraceID().String())
c.Set(constants.HeaderSpanID, spanCtx.SpanID().String())
c.Next()
}
}
type bodyLogWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
// TODO 包装一下 gin.ResponseWriter通过这种方式拦截写响应
// 让gin写响应的时候先写到 bodyLogWriter 再写gin.ResponseWriter
// 这样利用中间件里输出访问日志时就能拿到响应了
// https://stackoverflow.com/questions/38501325/how-to-log-response-body-in-gin
func (w bodyLogWriter) Write(b []byte) (int, error) {
w.body.Write(b)
return w.ResponseWriter.Write(b)
}
// TODO 用于访问request与reponse的日志记录
// LogAccess define func of log access info
func LogAccess() gin.HandlerFunc {
return func(c *gin.Context) {
// 保存body
var reqBody []byte
contentType := c.GetHeader("Content-Type")
// multipart/form-data 文件上传请求, 不在日志里记录body
if !strings.Contains(contentType, "multipart/form-data") {
reqBody, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewReader(reqBody))
// var request map[string]interface{}
// if err := c.ShouldBindBodyWith(&request, binding.JSON); err != nil {
// c.JSON(400, gin.H{"error": err.Error()})
// return
// }
}
start := time.Now()
blw := &bodyLogWriter{body: bytes.NewBufferString(""), ResponseWriter: c.Writer}
c.Writer = blw
accessLog(c, "access_start", time.Since(start), reqBody, nil)
defer func() {
var responseLogging string
if c.Writer.Size() > 10*1024 { // 响应大于10KB 不记录
responseLogging = "Response data size is too Large to log"
} else {
responseLogging = blw.body.String()
}
accessLog(c, "access_end", time.Since(start), reqBody, responseLogging)
}()
c.Next()
}
}
func accessLog(c *gin.Context, accessType string, dur time.Duration, body []byte, dataOut interface{}) {
req := c.Request
bodyStr := string(body)
query := req.URL.RawQuery
path := req.URL.Path
// TODO: 实现Token认证后再把访问日志里也加上token记录
// token := c.Request.Header.Get("token")
logger.New(c).Info("AccessLog",
"type", accessType,
"ip", c.ClientIP(),
//"token", token,
"method", req.Method,
"path", path,
"query", query,
"body", bodyStr,
"output", dataOut,
"time(ms)", int64(dur/time.Millisecond))
}