258 lines
7.7 KiB
Go
258 lines
7.7 KiB
Go
package otelTrace
|
||
|
||
import (
|
||
"context"
|
||
"github.com/beego/beego/v2/core/config/env"
|
||
"log/slog"
|
||
"net/http"
|
||
"os"
|
||
|
||
"github.com/beego/beego/v2/server/web"
|
||
beecontext "github.com/beego/beego/v2/server/web/context"
|
||
"go.opentelemetry.io/contrib/bridges/otelzap"
|
||
"go.opentelemetry.io/otel"
|
||
"go.opentelemetry.io/otel/attribute"
|
||
"go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp"
|
||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp"
|
||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace"
|
||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
|
||
"go.opentelemetry.io/otel/log/global"
|
||
"go.opentelemetry.io/otel/propagation"
|
||
sdklog "go.opentelemetry.io/otel/sdk/log"
|
||
sdkMetric "go.opentelemetry.io/otel/sdk/metric"
|
||
"go.opentelemetry.io/otel/sdk/resource"
|
||
sdktrace "go.opentelemetry.io/otel/sdk/trace"
|
||
"go.uber.org/zap"
|
||
"go.uber.org/zap/zapcore"
|
||
|
||
"time"
|
||
|
||
"go.opentelemetry.io/otel/trace"
|
||
)
|
||
|
||
// InitCtx 初始化ctx
|
||
var (
|
||
InitCtx = context.Background()
|
||
)
|
||
|
||
type LoggerStruct struct {
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// WithContext 为日志记录器添加上下文信息。
|
||
// 如果上下文中包含跟踪 id,则将其添加到日志中以增强可追踪性。
|
||
func (l *LoggerStruct) WithContext(ctx context.Context) *zap.Logger {
|
||
// 如果上下文为空,直接返回日志记录器,不进行任何修改。
|
||
if ctx == nil {
|
||
return l.logger
|
||
}
|
||
// 从 context 中获取 span
|
||
// 从 context 中获取 span
|
||
span := trace.SpanFromContext(ctx)
|
||
if !span.SpanContext().IsValid() {
|
||
if l.logger == nil {
|
||
return zap.NewNop()
|
||
}
|
||
return l.logger
|
||
}
|
||
return l.logger.With(zap.Reflect("ctx", ctx))
|
||
}
|
||
|
||
var (
|
||
serviceName = "网关服务——" + env.Get("serverName", "")
|
||
collectorURL = "otel-collector.kkknametrans.buzz"
|
||
Logger *LoggerStruct // 添加全局 logger
|
||
)
|
||
|
||
func InitTracer() (func(context.Context) error, func(context.Context) error, func(context.Context) error) {
|
||
traceExporter, err := otlptrace.New(
|
||
InitCtx,
|
||
otlptracehttp.NewClient(
|
||
otlptracehttp.WithInsecure(),
|
||
otlptracehttp.WithCompression(otlptracehttp.GzipCompression),
|
||
otlptracehttp.WithEndpoint(collectorURL),
|
||
),
|
||
)
|
||
|
||
if err != nil {
|
||
slog.ErrorContext(InitCtx, "failed to create trace exporter", slog.String("error", err.Error()))
|
||
return nil, nil, nil
|
||
}
|
||
resources, err := resource.New(
|
||
InitCtx,
|
||
resource.WithAttributes(
|
||
attribute.String("service.name", serviceName),
|
||
attribute.String("library.language", "go"),
|
||
),
|
||
)
|
||
if err != nil {
|
||
slog.ErrorContext(InitCtx, "failed to create resources", slog.String("error", err.Error()))
|
||
return nil, nil, nil
|
||
}
|
||
|
||
otel.SetTracerProvider(
|
||
sdktrace.NewTracerProvider(
|
||
sdktrace.WithSampler(sdktrace.AlwaysSample()),
|
||
sdktrace.WithSpanProcessor(sdktrace.NewBatchSpanProcessor(traceExporter,
|
||
sdktrace.WithBatchTimeout(30*time.Second),
|
||
sdktrace.WithMaxExportBatchSize(100),
|
||
)),
|
||
sdktrace.WithBatcher(traceExporter),
|
||
sdktrace.WithResource(resources),
|
||
),
|
||
)
|
||
// Set up meter provider.
|
||
metricExporter, err := otlpmetrichttp.New(
|
||
InitCtx,
|
||
otlpmetrichttp.WithInsecure(),
|
||
otlpmetrichttp.WithCompression(otlpmetrichttp.GzipCompression),
|
||
otlpmetrichttp.WithEndpoint(collectorURL))
|
||
if err != nil {
|
||
slog.ErrorContext(InitCtx, "failed to create trace exporter", slog.String("error", err.Error()))
|
||
return nil, nil, nil
|
||
}
|
||
otel.SetMeterProvider(
|
||
sdkMetric.NewMeterProvider(
|
||
sdkMetric.WithReader(
|
||
sdkMetric.NewPeriodicReader(
|
||
metricExporter,
|
||
sdkMetric.WithInterval(30*time.Second),
|
||
),
|
||
),
|
||
sdkMetric.WithResource(resources),
|
||
),
|
||
)
|
||
|
||
logExporter, err := otlploghttp.New(
|
||
InitCtx,
|
||
otlploghttp.WithCompression(otlploghttp.GzipCompression),
|
||
otlploghttp.WithInsecure(),
|
||
otlploghttp.WithEndpoint(collectorURL),
|
||
)
|
||
if err != nil {
|
||
slog.ErrorContext(InitCtx, "failed to create log exporter", slog.String("error", err.Error()))
|
||
return nil, nil, nil
|
||
}
|
||
loggerProvider := sdklog.NewLoggerProvider(
|
||
sdklog.WithProcessor(
|
||
sdklog.NewBatchProcessor(logExporter, sdklog.WithExportMaxBatchSize(100)),
|
||
),
|
||
sdklog.WithResource(resources),
|
||
)
|
||
global.SetLoggerProvider(loggerProvider)
|
||
|
||
// 自定义日志格式配置
|
||
encoderConfig := zapcore.EncoderConfig{
|
||
TimeKey: "time",
|
||
LevelKey: "level",
|
||
NameKey: "logger",
|
||
CallerKey: "caller",
|
||
FunctionKey: zapcore.OmitKey,
|
||
MessageKey: "msg",
|
||
StacktraceKey: "stacktrace",
|
||
LineEnding: zapcore.DefaultLineEnding,
|
||
EncodeLevel: zapcore.CapitalLevelEncoder, // 使用大写字母记录日志级别
|
||
EncodeTime: zapcore.TimeEncoderOfLayout(time.DateTime), // ISO8601 时间格式
|
||
EncodeDuration: zapcore.SecondsDurationEncoder,
|
||
EncodeCaller: zapcore.ShortCallerEncoder, // 短路径编码器
|
||
}
|
||
|
||
// 创建核心
|
||
core := zapcore.NewTee(
|
||
otelzap.NewCore(serviceName, otelzap.WithLoggerProvider(loggerProvider)),
|
||
zapcore.NewCore(
|
||
zapcore.NewConsoleEncoder(encoderConfig),
|
||
// zapcore.NewJSONEncoder(zap.NewProductionEncoderConfig()),
|
||
zapcore.NewMultiWriteSyncer(
|
||
zapcore.AddSync(os.Stdout),
|
||
),
|
||
zap.InfoLevel,
|
||
),
|
||
)
|
||
logger := zap.New(core,
|
||
zap.AddCaller(),
|
||
zap.AddStacktrace(zap.ErrorLevel),
|
||
)
|
||
// zap设置标准输出流
|
||
// 设置全局 logger
|
||
Logger = &LoggerStruct{
|
||
logger: logger,
|
||
}
|
||
// 确保设置 TextMapPropagator
|
||
otel.SetTextMapPropagator(
|
||
propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}),
|
||
)
|
||
return traceExporter.Shutdown, metricExporter.Shutdown, logExporter.Shutdown
|
||
}
|
||
|
||
// Middleware 从请求头中提取上游的 trace context
|
||
// 并将其注入到当前的 context 中
|
||
// 这样下游的服务就可以从 context 中获取到上游的 trace context
|
||
// 这样就可以在日志中看到上游的 trace Context
|
||
func Middleware(ctx *beecontext.Context, next web.FilterFunc) {
|
||
req := ctx.Request
|
||
// 从请求头中提取上游的 trace context
|
||
propagator := otel.GetTextMapPropagator()
|
||
parentCtx := propagator.Extract(req.Context(), propagation.HeaderCarrier(req.Header))
|
||
routeTrace := otel.Tracer("router")
|
||
spanCtx, span := routeTrace.Start(parentCtx, req.URL.Path,
|
||
trace.WithAttributes(
|
||
attribute.String("http.method", req.Method),
|
||
attribute.String("http.url", req.URL.String()),
|
||
attribute.String("peer.hostname", req.Host),
|
||
attribute.String("http.scheme", getScheme(req)),
|
||
attribute.String("component", "beego"),
|
||
attribute.String("peer.address", req.RemoteAddr),
|
||
attribute.String("span.kind", "server"),
|
||
attribute.String("peer.service", serviceName),
|
||
attribute.String("http.flavor", req.Proto),
|
||
attribute.String("http.user_agent", req.UserAgent()),
|
||
attribute.String("http.target", req.URL.Path),
|
||
),
|
||
)
|
||
// 合并所有 defer 处理到一个函数中
|
||
defer func() {
|
||
// 先处理状态码
|
||
if ctx.Request.Response != nil {
|
||
span.SetAttributes(attribute.Int("http.status_code", ctx.Request.Response.StatusCode))
|
||
}
|
||
|
||
// 处理 panic
|
||
if err := recover(); err != nil {
|
||
span.RecordError(err.(error))
|
||
span.SetAttributes(attribute.String("error", "true"))
|
||
Logger.WithContext(ctx.Request.Context()).Error("全局错误", zap.Any("error", err))
|
||
// 结束 span
|
||
span.End()
|
||
// 重新抛出异常
|
||
panic(err)
|
||
}
|
||
// 正常情况下结束 span
|
||
span.End()
|
||
}()
|
||
ctx.Request = ctx.Request.WithContext(spanCtx)
|
||
next(ctx)
|
||
}
|
||
|
||
// Span 抽象span
|
||
func Span(ctx context.Context, traceName, spanName string, attr ...trace.SpanStartOption) (context.Context, func()) {
|
||
ctx, span := otel.Tracer(traceName).Start(ctx, spanName, attr...)
|
||
return ctx, func() {
|
||
defer span.End()
|
||
}
|
||
}
|
||
|
||
func getScheme(req *http.Request) string {
|
||
scheme := req.Header.Get("X-Forwarded-Proto")
|
||
if scheme == "" && req.TLS != nil {
|
||
scheme = "https"
|
||
} else if scheme == "" {
|
||
scheme = "http"
|
||
}
|
||
return scheme
|
||
}
|
||
|
||
func NewSchedulerTrace(opts ...trace.TracerOption) trace.Tracer {
|
||
return otel.Tracer("scheduler", opts...)
|
||
}
|