产品目录服务

此服务负责返回有关产品的信息。该服务可用于获取所有产品、搜索特定产品或返回单个产品的详细信息。

产品目录服务源码

追踪

初始化追踪

OpenTelemetry SDK 在 `main` 中通过 `initTracerProvider` 函数进行初始化。

func initTracerProvider() *sdktrace.TracerProvider {
    ctx := context.Background()

    exporter, err := otlptracegrpc.New(ctx)
    if err != nil {
        log.Fatalf("OTLP Trace gRPC Creation: %v", err)
    }
    tp := sdktrace.NewTracerProvider(
        sdktrace.WithBatcher(exporter),
        sdktrace.WithResource(initResource()),
    )
    otel.SetTracerProvider(tp)
    otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}))
    return tp
}

在服务关闭时,您应该调用 `TracerProvider.Shutdown()` 以确保所有 Span 都被导出。此服务在 main 函数的延迟函数中调用了该函数。

tp := InitTracerProvider()
defer func() {
    if err := tp.Shutdown(context.Background()); err != nil {
        log.Fatalf("Tracer Provider Shutdown: %v", err)
    }
}()

添加 gRPC 自动仪表化

此服务接收 gRPC 请求,这些请求在 main 函数中作为 gRPC 服务器创建的一部分进行仪表化。

srv := grpc.NewServer(
    grpc.StatsHandler(otelgrpc.NewServerHandler()),
)

此服务将发出出站 gRPC 调用,所有这些调用都通过包装 gRPC 客户端的仪表化来完成。

func createClient(ctx context.Context, svcAddr string) (*grpc.ClientConn, error) {
    return grpc.DialContext(ctx, svcAddr,
        grpc.WithTransportCredentials(insecure.NewCredentials()),
        grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
    )
}

为自动仪表化的 Span 添加属性

在自动仪表化代码的执行过程中,您可以从上下文中获取当前的 Span。

span := trace.SpanFromContext(ctx)

通过 Span 对象上的 `SetAttributes` 来添加 Span 属性。在 `GetProduct` 函数中,为产品 ID 添加了一个属性到 Span 中。

span.SetAttributes(
    attribute.String("app.product.id", req.Id),
)

设置 Span 状态

此服务可以根据功能标志捕获和处理错误情况。在错误情况下,Span 状态会使用 Span 对象上的 `SetStatus` 进行相应设置。您可以在 `GetProduct` 函数中看到这一点。

msg := fmt.Sprintf("Error: ProductCatalogService Fail Feature Flag Enabled")
span.SetStatus(otelcodes.Error, msg)

添加跨度事件

通过 Span 对象上的 `AddEvent` 来添加 Span 事件。在 `GetProduct` 函数中,当处理错误情况或成功找到产品时,会添加一个 Span 事件。

span.AddEvent(msg)

指标

初始化指标

OpenTelemetry SDK 在 `main` 中通过 `initMeterProvider` 函数进行初始化。

func initMeterProvider() *sdkmetric.MeterProvider {
    ctx := context.Background()

    exporter, err := otlpmetricgrpc.New(ctx)
    if err != nil {
        log.Fatalf("new otlp metric grpc exporter failed: %v", err)
    }

    mp := sdkmetric.NewMeterProvider(sdkmetric.WithReader(sdkmetric.NewPeriodicReader(exporter)))
    global.SetMeterProvider(mp)
    return mp
}

在服务关闭时,您应该调用 `initMeterProvider.Shutdown()` 以确保所有记录都被导出。此服务在 main 函数的延迟函数中调用了该函数。

mp := initMeterProvider()
defer func() {
    if err := mp.Shutdown(context.Background()); err != nil {
        log.Fatalf("Error shutting down meter provider: %v", err)
    }
}()

添加 Go 运行时自动仪表化

Go 运行时在 main 函数中进行了仪表化。

err := runtime.Start(runtime.WithMinimumReadMemStatsInterval(time.Second))
if err != nil {
    log.Fatal(err)
}

日志

您可以通过两种方式将日志发送到 OpenTelemetry Collector:

  • 直接发送到 Collector
  • 通过文件或 `stdout`

您可以在 日志 部分的 手动仪表化 文档中找到有关如何使用这两种方法的文档。

产品目录服务直接将日志发送到 Collector,并使用日志桥接器发送其日志,该日志桥接器连接到 `slog` 日志包,并输出结构化日志。

LoggerProvider 初始化

OpenTelemetry SDK 在 `main` 中通过 `initLoggerProvider` 函数进行初始化。

ctx := context.Background()

logExporter, err := otlploggrpc.New(ctx)
if err != nil {
	return nil
}

loggerProvider := sdklog.NewLoggerProvider(
	sdklog.WithProcessor(sdklog.NewBatchProcessor(logExporter)),
)
global.SetLoggerProvider(loggerProvider)

return loggerProvider

在服务关闭时,您应该调用 `LoggerProvider.Shutdown()` 以确保所有日志都被导出。此服务在 `main` 函数的延迟函数中调用了该函数。

lp := initLoggerProvider()
defer func() {
	if err := lp.Shutdown(context.Background()); err != nil {
		logger.Error(fmt.Sprintf("Logger Provider Shutdown: %v", err))
	}
	logger.Info("Shutdown logger provider")
}()

日志功能

此服务使用 gRPC 调用将日志发送到 Collector。日志使用 `slog` 包以结构化格式输出。

首先,初始化 logger

logger   *slog.Logger
logger = otelslog.NewLogger("product-catalog")

请注意使用 `fmt.Sprintf` 在发送到 logger 之前格式化输出。

logger.Info("Loading Product Catalog...")
logger.Info(fmt.Sprintf("Product Catalog reload interval: %d", interval))
logger.Error(fmt.Sprintf("Error shutting down meter provider: %v", err))

使用 `slog` 的优点是可以将其他属性附加到输出中。以下示例附加了 `product.name` 和 `product.id` 属性。这使得可以在日志输出中查看和解析这些属性,并在 Grafana 中更轻松地将它们显示为单独的列。

logger.LogAttrs(
	ctx,
	slog.LevelInfo, "Product Found",
	slog.String("app.product.name", found.Name),
	slog.String("app.product.id", req.Id),
)

最后修改日期 2025 年 7 月 24 日: 为产品目录添加日志部分 (#7380) (65884372)