This commit is contained in:
ryan
2026-06-16 09:20:18 +08:00
parent bfa00ae6ac
commit c6df73e522
17 changed files with 183 additions and 25 deletions
+1
View File
@@ -27,3 +27,4 @@ frontend/*.tsbuildinfo
frontend/package-lock.json frontend/package-lock.json
internal/router/dist/ internal/router/dist/
internal/router/root/dist/
+10 -2
View File
@@ -8,6 +8,9 @@
APP_PORT=8000 APP_PORT=8000
POSTGRES_PORT=5432 POSTGRES_PORT=5432
REDIS_PORT=6379 REDIS_PORT=6379
JAEGER_UI_PORT=16686
JAEGER_OTLP_GRPC_PORT=4317
JAEGER_OTLP_HTTP_PORT=4318
# ─── PostgreSQL 容器配置(仅 docker-compose 使用)──────────────────────────── # ─── PostgreSQL 容器配置(仅 docker-compose 使用)────────────────────────────
POSTGRES_DB=wavelet POSTGRES_DB=wavelet
@@ -72,8 +75,13 @@ LOG_FORMAT=console
LOG_OUTPUT=stdout LOG_OUTPUT=stdout
# ─── OpenTelemetry ───────────────────────────────────────────────────────────── # ─── OpenTelemetry ─────────────────────────────────────────────────────────────
# 设为 0 关闭 tracing(无 Collector 时推荐) # docker-compose 默认将 Trace 发往 Jaeger All-in-One: http://jaeger:4317
OTEL_SAMPLING_RATE=0.0 OTEL_EXPORTER_OTLP_ENDPOINT=http://jaeger:4317
OTEL_EXPORTER_OTLP_INSECURE=true
# 设为 0 关闭 tracing;本地 Jaeger 调试建议设为 1.0
OTEL_SAMPLING_RATE=1.0
# 全局 Tracer 命名空间,默认为 github.com/Rain-kl/Wavelet
# OTEL_TRACER_NAME=github.com/Rain-kl/Wavelet
# ─── S3 兼容存储(可选,默认关闭)────────────────────────────────────────── # ─── S3 兼容存储(可选,默认关闭)──────────────────────────────────────────
# S3_ENABLED=false # S3_ENABLED=false
+2
View File
@@ -95,6 +95,8 @@ worker:
# ─── OpenTelemetry Tracing ────────────────────────────────────────────────────── # ─── OpenTelemetry Tracing ──────────────────────────────────────────────────────
otel: otel:
sampling_rate: 0.0 # Trace sampling rate (0.0 – 1.0) sampling_rate: 0.0 # Trace sampling rate (0.0 – 1.0)
tracer_name: "github.com/Rain-kl/Wavelet" # Global tracer instrumentation name
# ─── ClickHouse (optional) ────────────────────────────────────────────────────── # ─── ClickHouse (optional) ──────────────────────────────────────────────────────
clickhouse: clickhouse:
+21 -6
View File
@@ -1,15 +1,18 @@
services: services:
wavelet: wavelet:
# build: build:
# context: . context: .
# dockerfile: docker/Dockerfile dockerfile: docker/Dockerfile
# args: args:
# VERSION: v0.9.9 VERSION: v0.9.9
image: ghcr.io/rain-kl/wavelet:latest # image: ghcr.io/rain-kl/wavelet:latest
restart: unless-stopped restart: unless-stopped
env_file: .env env_file: .env
environment: environment:
TZ: ${TZ:-Asia/Shanghai} TZ: ${TZ:-Asia/Shanghai}
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://jaeger:4317}
OTEL_EXPORTER_OTLP_INSECURE: ${OTEL_EXPORTER_OTLP_INSECURE:-true}
OTEL_SAMPLING_RATE: ${OTEL_SAMPLING_RATE:-1.0}
ports: ports:
- "${APP_PORT:-8000}:8000" - "${APP_PORT:-8000}:8000"
volumes: volumes:
@@ -20,6 +23,8 @@ services:
condition: service_healthy condition: service_healthy
redis: redis:
condition: service_healthy condition: service_healthy
jaeger:
condition: service_started
postgres: postgres:
image: postgres:17-alpine image: postgres:17-alpine
@@ -54,6 +59,16 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
start_period: 5s start_period: 5s
jaeger:
image: jaegertracing/jaeger:${JAEGER_VERSION:-2.19.0}
restart: unless-stopped
environment:
TZ: ${TZ:-Asia/Shanghai}
ports:
- "${JAEGER_UI_PORT:-16686}:16686"
- "${JAEGER_OTLP_GRPC_PORT:-4317}:4317"
- "${JAEGER_OTLP_HTTP_PORT:-4318}:4318"
# #
# clickhouse: # clickhouse:
# image: clickhouse/clickhouse-server:25.3-alpine # image: clickhouse/clickhouse-server:25.3-alpine
+1 -1
View File
@@ -40,7 +40,7 @@ COPY go.mod go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
COPY --from=frontend-builder /workspace/frontend/out ./internal/router/dist COPY --from=frontend-builder /workspace/frontend/out ./internal/router/root/dist
RUN CGO_ENABLED=0 GOOS=linux go build \ RUN CGO_ENABLED=0 GOOS=linux go build \
-tags embed_frontend \ -tags embed_frontend \
+1 -1
View File
@@ -72,7 +72,7 @@ RUN go mod download
COPY . . COPY . .
# Overlay the compiled frontend into the embed path # Overlay the compiled frontend into the embed path
COPY --from=frontend-builder /workspace/frontend/out ./internal/router/dist COPY --from=frontend-builder /workspace/frontend/out ./internal/router/root/dist
# Build matrix: GOOS × GOARCH # Build matrix: GOOS × GOARCH
# CGO_ENABLED=0 — fully static, no libc dependency, required for cross-compilation. # CGO_ENABLED=0 — fully static, no libc dependency, required for cross-compilation.
+14
View File
@@ -5,7 +5,9 @@
package cmd package cmd
import ( import (
"context"
"log" "log"
"time"
"github.com/Rain-kl/Wavelet/internal/buildinfo" "github.com/Rain-kl/Wavelet/internal/buildinfo"
"github.com/Rain-kl/Wavelet/internal/config" "github.com/Rain-kl/Wavelet/internal/config"
@@ -15,6 +17,8 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
const traceShutdownTimeout = 10 * time.Second
var rootCmd = &cobra.Command{ var rootCmd = &cobra.Command{
Use: "wavelet", Use: "wavelet",
PersistentPreRun: func(_ *cobra.Command, _ []string) { PersistentPreRun: func(_ *cobra.Command, _ []string) {
@@ -31,11 +35,15 @@ var rootCmd = &cobra.Command{
trace.Init(trace.Config{ trace.Init(trace.Config{
AppName: config.Config.App.AppName, AppName: config.Config.App.AppName,
SamplingRate: config.Config.Otel.SamplingRate, SamplingRate: config.Config.Otel.SamplingRate,
TracerName: config.Config.Otel.TracerName,
}) })
}, },
PreRun: func(_ *cobra.Command, _ []string) { PreRun: func(_ *cobra.Command, _ []string) {
migrator.Migrate() migrator.Migrate()
}, },
PersistentPostRun: func(_ *cobra.Command, _ []string) {
shutdownTraceProvider()
},
Run: func(_ *cobra.Command, args []string) { Run: func(_ *cobra.Command, args []string) {
// 无参数时默认以融合模式启动所有服务 // 无参数时默认以融合模式启动所有服务
if len(args) == 0 { if len(args) == 0 {
@@ -58,6 +66,12 @@ var rootCmd = &cobra.Command{
}, },
} }
func shutdownTraceProvider() {
ctx, cancel := context.WithTimeout(context.Background(), traceShutdownTimeout)
defer cancel()
trace.Shutdown(ctx)
}
func init() { func init() {
rootCmd.Version = buildinfo.Version rootCmd.Version = buildinfo.Version
rootCmd.CompletionOptions.DisableDefaultCmd = true rootCmd.CompletionOptions.DisableDefaultCmd = true
+4
View File
@@ -112,6 +112,9 @@ func applyDefaults(c *configModel) {
if c.App.SessionAge <= 0 { if c.App.SessionAge <= 0 {
c.App.SessionAge = 86400 c.App.SessionAge = 86400
} }
if c.Otel.TracerName == "" {
c.Otel.TracerName = "github.com/Rain-kl/Wavelet"
}
} }
// ─── 环境变量覆盖层 ──────────────────────────────────────────────────────────── // ─── 环境变量覆盖层 ────────────────────────────────────────────────────────────
@@ -223,6 +226,7 @@ func applyEnvOverrides(c *configModel) {
// ─── OTel ─── // ─── OTel ───
c.Otel.SamplingRate = envFloat64("OTEL_SAMPLING_RATE", c.Otel.SamplingRate) c.Otel.SamplingRate = envFloat64("OTEL_SAMPLING_RATE", c.Otel.SamplingRate)
c.Otel.TracerName = envStr("OTEL_TRACER_NAME", c.Otel.TracerName)
// ─── Worker ─── // ─── Worker ───
c.Worker.Concurrency = envInt("WORKER_CONCURRENCY", c.Worker.Concurrency) c.Worker.Concurrency = envInt("WORKER_CONCURRENCY", c.Worker.Concurrency)
+1
View File
@@ -137,4 +137,5 @@ type QueueConfig struct {
// otelConfig OpenTelemetry 配置 // otelConfig OpenTelemetry 配置
type otelConfig struct { type otelConfig struct {
SamplingRate float64 `mapstructure:"sampling_rate"` SamplingRate float64 `mapstructure:"sampling_rate"`
TracerName string `mapstructure:"tracer_name"`
} }
+13
View File
@@ -55,6 +55,19 @@ func initSQLite() {
log.Fatalf("[SQLite] init connection failed: %v\n", err) log.Fatalf("[SQLite] init connection failed: %v\n", err)
} }
// Trace 注入
if err = db.Use(
tracing.NewPlugin(
tracing.WithoutMetrics(),
tracing.WithAttributes(
attribute.String("db.instance", sqlitePath),
attribute.String("db.system", "SQLite"),
),
),
); err != nil {
log.Fatalf("[SQLite] init trace failed: %v\n", err)
}
log.Printf("[SQLite] initialized (path: %s)\n", sqlitePath) log.Printf("[SQLite] initialized (path: %s)\n", sqlitePath)
} }
+59 -7
View File
@@ -6,6 +6,7 @@ package task
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"time" "time"
@@ -15,8 +16,10 @@ import (
"github.com/Rain-kl/Wavelet/pkg/logger" "github.com/Rain-kl/Wavelet/pkg/logger"
otel_trace "github.com/Rain-kl/Wavelet/pkg/trace" otel_trace "github.com/Rain-kl/Wavelet/pkg/trace"
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
@@ -56,6 +59,14 @@ func ValidateAndNormalizePayload(asynqTaskType string, payload []byte) ([]byte,
type contextKey string type contextKey string
const taskIDKey contextKey = "task_execution_task_id" const taskIDKey contextKey = "task_execution_task_id"
const traceEnvelopeVersion = 1
type traceEnvelope struct {
WaveletTraceEnvelope bool `json:"_wavelet_trace_envelope"`
Version int `json:"version"`
TraceContext map[string]string `json:"trace_context,omitempty"`
Payload []byte `json:"payload"`
}
// withTaskID 将 taskID 注入 context // withTaskID 将 taskID 注入 context
func withTaskID(ctx context.Context, taskID string) context.Context { func withTaskID(ctx context.Context, taskID string) context.Context {
@@ -124,7 +135,7 @@ func DispatchTask(ctx context.Context, taskType string, payload []byte, triggere
} }
// 入队 Asynq // 入队 Asynq
taskInfo := asynq.NewTask(meta.AsynqTask, payload) taskInfo := asynq.NewTask(meta.AsynqTask, injectTaskTraceContext(ctx, payload))
if _, err := AsynqClient.Enqueue( if _, err := AsynqClient.Enqueue(
taskInfo, taskInfo,
asynq.TaskID(taskID), asynq.TaskID(taskID),
@@ -190,7 +201,7 @@ func RetryTask(ctx context.Context, id uint64) (string, error) {
} }
// 入队 Asynq // 入队 Asynq
taskInfo := asynq.NewTask(execution.TaskType, []byte(execution.Payload)) taskInfo := asynq.NewTask(execution.TaskType, injectTaskTraceContext(ctx, []byte(execution.Payload)))
if _, err := AsynqClient.Enqueue( if _, err := AsynqClient.Enqueue(
taskInfo, taskInfo,
asynq.TaskID(newTaskID), asynq.TaskID(newTaskID),
@@ -216,6 +227,9 @@ func RetryTask(ctx context.Context, id uint64) (string, error) {
// ProcessTask Asynq 实际调用的统一处理函数 // ProcessTask Asynq 实际调用的统一处理函数
// Worker 注册时统一使用此函数,内部自动分发到对应的 TaskHandler // Worker 注册时统一使用此函数,内部自动分发到对应的 TaskHandler
func ProcessTask(ctx context.Context, t *asynq.Task) error { func ProcessTask(ctx context.Context, t *asynq.Task) error {
taskPayload := t.Payload()
ctx, taskPayload, hasRemoteTraceContext := extractTaskTraceContext(ctx, taskPayload)
// 初始化 Trace // 初始化 Trace
ctx, span := otel_trace.Start(ctx, "TaskProcess_"+t.Type(), trace.WithSpanKind(trace.SpanKindConsumer)) ctx, span := otel_trace.Start(ctx, "TaskProcess_"+t.Type(), trace.WithSpanKind(trace.SpanKindConsumer))
defer span.End() defer span.End()
@@ -223,7 +237,8 @@ func ProcessTask(ctx context.Context, t *asynq.Task) error {
// 添加任务信息到 Span // 添加任务信息到 Span
span.SetAttributes( span.SetAttributes(
attribute.String("task.type", t.Type()), attribute.String("task.type", t.Type()),
attribute.Int("task.payload_size", len(t.Payload())), attribute.Int("task.payload_size", len(taskPayload)),
attribute.Bool("task.trace_context_propagated", hasRemoteTraceContext),
attribute.String("task.id", t.ResultWriter().TaskID()), attribute.String("task.id", t.ResultWriter().TaskID()),
) )
@@ -243,7 +258,7 @@ func ProcessTask(ctx context.Context, t *asynq.Task) error {
// 加载或动态创建执行记录 // 加载或动态创建执行记录
now := time.Now() now := time.Now()
execution, err := getOrCreateTaskExecution(ctx, taskID, t, now) execution, err := getOrCreateTaskExecution(ctx, taskID, t, taskPayload, now)
if err == nil { if err == nil {
updateExecutionOnStart(ctx, execution, now) updateExecutionOnStart(ctx, execution, now)
} }
@@ -259,7 +274,7 @@ func ProcessTask(ctx context.Context, t *asynq.Task) error {
start := time.Now() start := time.Now()
// 执行业务逻辑 // 执行业务逻辑
result, execErr := handler.Execute(ctx, t.Payload()) result, execErr := handler.Execute(ctx, taskPayload)
// 计算耗时并归档记录 // 计算耗时并归档记录
duration := time.Since(start) duration := time.Since(start)
@@ -275,6 +290,43 @@ func ProcessTask(ctx context.Context, t *asynq.Task) error {
return execErr return execErr
} }
func injectTaskTraceContext(ctx context.Context, payload []byte) []byte {
carrier := propagation.MapCarrier{}
otel.GetTextMapPropagator().Inject(ctx, carrier)
if len(carrier) == 0 {
return payload
}
envelope := traceEnvelope{
WaveletTraceEnvelope: true,
Version: traceEnvelopeVersion,
TraceContext: map[string]string(carrier),
Payload: payload,
}
data, err := json.Marshal(envelope)
if err != nil {
logger.ErrorF(ctx, "[TaskExecutor] 序列化任务 Trace 上下文失败: %v", err)
return payload
}
return data
}
func extractTaskTraceContext(ctx context.Context, payload []byte) (context.Context, []byte, bool) {
var envelope traceEnvelope
if err := json.Unmarshal(payload, &envelope); err != nil {
return ctx, payload, false
}
if !envelope.WaveletTraceEnvelope || envelope.Version != traceEnvelopeVersion {
return ctx, payload, false
}
if len(envelope.TraceContext) == 0 {
return ctx, envelope.Payload, false
}
extractedCtx := otel.GetTextMapPropagator().Extract(ctx, propagation.MapCarrier(envelope.TraceContext))
return extractedCtx, envelope.Payload, true
}
func updateExecutionOnStart(ctx context.Context, execution *model.TaskExecution, now time.Time) { func updateExecutionOnStart(ctx context.Context, execution *model.TaskExecution, now time.Time) {
if execution == nil { if execution == nil {
return return
@@ -297,7 +349,7 @@ func updateExecutionOnStart(ctx context.Context, execution *model.TaskExecution,
} }
// getOrCreateTaskExecution 获取已有的任务执行记录,如果不存在则针对已知任务类型动态创建记录 // getOrCreateTaskExecution 获取已有的任务执行记录,如果不存在则针对已知任务类型动态创建记录
func getOrCreateTaskExecution(ctx context.Context, taskID string, t *asynq.Task, now time.Time) (*model.TaskExecution, error) { func getOrCreateTaskExecution(ctx context.Context, taskID string, t *asynq.Task, payload []byte, now time.Time) (*model.TaskExecution, error) {
execution, err := model.GetTaskExecutionByTaskID(ctx, taskID) execution, err := model.GetTaskExecutionByTaskID(ctx, taskID)
if err == nil { if err == nil {
return execution, nil return execution, nil
@@ -316,7 +368,7 @@ func getOrCreateTaskExecution(ctx context.Context, taskID string, t *asynq.Task,
Retryable: meta.Retryable, Retryable: meta.Retryable,
MaxRetry: meta.MaxRetry, MaxRetry: meta.MaxRetry,
RetryCount: 0, RetryCount: 0,
Payload: string(t.Payload()), Payload: string(payload),
TriggeredBy: "schedule", TriggeredBy: "schedule",
StartedAt: &now, StartedAt: &now,
} }
+37
View File
@@ -15,6 +15,8 @@ import (
"github.com/hibiken/asynq" "github.com/hibiken/asynq"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
) )
@@ -133,6 +135,41 @@ func TestAppendLogWithTaskID(t *testing.T) {
assert.Contains(t, found.Log, "处理了 50 条数据") assert.Contains(t, found.Log, "处理了 50 条数据")
} }
func TestTaskTraceContextEnvelope(t *testing.T) {
payload := []byte(`{"hello":"wavelet"}`)
traceID := "4bf92f3577b34da6a3ce929d0e0e4736"
parentCtx := otel.GetTextMapPropagator().Extract(
context.Background(),
propagation.MapCarrier{
"traceparent": "00-" + traceID + "-00f067aa0ba902b7-01",
},
)
wrappedPayload := injectTaskTraceContext(parentCtx, payload)
require.NotEqual(t, string(payload), string(wrappedPayload))
gotCtx, gotPayload, ok := extractTaskTraceContext(context.Background(), wrappedPayload)
require.True(t, ok)
assert.Equal(t, payload, gotPayload)
assert.Equal(t, traceID, trace.SpanContextFromContext(gotCtx).TraceID().String())
}
func TestTaskTraceContextEnvelopeKeepsLegacyPayload(t *testing.T) {
payload := []byte(`{"legacy":true}`)
gotCtx, gotPayload, ok := extractTaskTraceContext(context.Background(), payload)
require.False(t, ok)
assert.Equal(t, context.Background(), gotCtx)
assert.Equal(t, payload, gotPayload)
}
func TestTaskTraceContextEnvelopeSkipsEmptyContext(t *testing.T) {
payload := []byte(`{"background":true}`)
wrappedPayload := injectTaskTraceContext(context.Background(), payload)
assert.Equal(t, payload, wrappedPayload)
}
func TestProcessTaskSuccess(t *testing.T) { func TestProcessTaskSuccess(t *testing.T) {
cleanup := setupTest(t) cleanup := setupTest(t)
defer cleanup() defer cleanup()
+3 -2
View File
@@ -11,7 +11,8 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/Rain-kl/Wavelet/pkg/httppool"
) )
func init() { func init() {
@@ -54,7 +55,7 @@ func (p *CustomPusher) Send(ctx context.Context, cfg Config, _ string, body map[
httpReq.Header.Set(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])) httpReq.Header.Set(strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]))
} }
client := &http.Client{Timeout: 10 * time.Second} client := httppool.NewClient(defaultHTTPClientTimeout)
resp, err := client.Do(httpReq) resp, err := client.Do(httpReq)
if err != nil { if err != nil {
return fmt.Errorf("custom: http request failed: %w", err) return fmt.Errorf("custom: http request failed: %w", err)
+3 -1
View File
@@ -16,6 +16,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/Rain-kl/Wavelet/pkg/httppool"
) )
func init() { func init() {
@@ -228,7 +230,7 @@ func (p *LarkPusher) Send(ctx context.Context, cfg Config, _ string, body map[st
} }
httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second} client := httppool.NewClient(defaultHTTPClientTimeout)
resp, err := client.Do(httpReq) resp, err := client.Do(httpReq)
if err != nil { if err != nil {
return fmt.Errorf("lark: http request failed: %w", err) return fmt.Errorf("lark: http request failed: %w", err)
+4 -2
View File
@@ -8,11 +8,13 @@ import (
"context" "context"
"fmt" "fmt"
"sync" "sync"
"time"
) )
const ( const (
defaultTitle = "系统通知" defaultTitle = "系统通知"
levelInfo = "INFO" levelInfo = "INFO"
defaultHTTPClientTimeout = 10 * time.Second
) )
// Config 基础通知渠道配置 // Config 基础通知渠道配置
+3 -2
View File
@@ -11,7 +11,8 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"strings" "strings"
"time"
"github.com/Rain-kl/Wavelet/pkg/httppool"
) )
func init() { func init() {
@@ -131,7 +132,7 @@ func (p *TelegramPusher) sendMessage(ctx context.Context, baseURL, token, chatID
} }
httpReq.Header.Set("Content-Type", "application/json") httpReq.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second} client := httppool.NewClient(defaultHTTPClientTimeout)
resp, err := client.Do(httpReq) resp, err := client.Do(httpReq)
if err != nil { if err != nil {
return fmt.Errorf("http request failed: %w", err) return fmt.Errorf("http request failed: %w", err)
+6 -1
View File
@@ -29,6 +29,7 @@ func init() {
type Config struct { type Config struct {
AppName string AppName string
SamplingRate float64 SamplingRate float64
TracerName string
} }
// Init 初始化 Tracer Provider 并关联全局 Tracer 实例 // Init 初始化 Tracer Provider 并关联全局 Tracer 实例
@@ -41,7 +42,11 @@ func Init(cfg Config) {
otel.SetTracerProvider(tracerProvider) otel.SetTracerProvider(tracerProvider)
// 更新 Tracer // 更新 Tracer
Tracer = tracerProvider.Tracer("github.com/Rain-kl/Wavelet") tracerName := cfg.TracerName
if tracerName == "" {
tracerName = "github.com/Rain-kl/Wavelet"
}
Tracer = tracerProvider.Tracer(tracerName)
} }
// Shutdown 关闭所有 Trace Provider // Shutdown 关闭所有 Trace Provider