结账服务

此服务负责处理用户的结账订单。结账服务在处理订单时会调用许多其他服务。

Checkout service source

追踪

初始化追踪

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

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

    exporter, err := otlptracegrpc.New(ctx)
    if err != nil {
        log.Fatal(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.Printf("Error shutting down tracer provider: %v", err)
    }
}()

添加 gRPC 自动检测

此服务接收 gRPC 请求,这些请求在 gRPC 服务器创建过程中在 main 函数中进行了检测。

var 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()),
    )
}

添加 Kafka (Sarama) 自动检测

此服务会将处理后的结果写入 Kafka 主题,然后由其他微服务进行处理。要检测 Kafka 客户端,必须在创建 Producer 后对其进行包装。

saramaConfig := sarama.NewConfig()
producer, err := sarama.NewAsyncProducer(brokers, saramaConfig)
if err != nil {
    return nil, err
}
producer = otelsarama.WrapAsyncProducer(saramaConfig, producer)

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

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

span := trace.SpanFromContext(ctx)

通过使用 Span 对象上的 SetAttributes 来添加属性到 Span。在 PlaceOrder 函数中,向 Span 添加了几个属性。

span.SetAttributes(
    attribute.String("app.order.id", orderID.String()), shippingTrackingAttribute,
    attribute.Float64("app.shipping.amount", shippingCostFloat),
    attribute.Float64("app.order.amount", totalPriceFloat),
    attribute.Int("app.order.items.count", len(prep.orderItems)),
)

添加跨度事件

通过使用 Span 对象上的 AddEvent 来添加 Span 事件。在 PlaceOrder 函数中,添加了几个 Span 事件。有些事件带有附加属性,有些则不带。

添加不带属性的 Span 事件

span.AddEvent("prepared")

添加带附加属性的 Span 事件

span.AddEvent("charged",
    trace.WithAttributes(attribute.String("app.payment.transaction.id", txID)))

指标

初始化指标

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
}

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

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

添加 golang 运行时自动检测

Golang 运行时在 main 函数中进行检测。

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

日志

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

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

您可以在 Manual Instrumentation 文档的 Logs 部分找到有关如何使用这两种方法的文档。

Checkout 服务将日志直接发送到 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   *slog.Logger
logger = otelslog.NewLogger("checkout")

注意使用 fmt.Sprintf 在将输出发送到日志记录器之前对其进行格式化。

logger.Info(fmt.Sprintf("order confirmation email sent to %q", req.Email))
logger.Warn(fmt.Sprintf("failed to send order confirmation to %q: %+v", req.Email, err))
logger.Error(fmt.Sprintf("Error shutting down logger provider: %v", err))

使用 slog 的优点是可以将其他属性附加到输出。下面的示例附加了几个属性,如 orderIDshippingCosttotalPrice。这使得可以将它们作为日志输出的一部分进行查看和解析,并且更容易在 Grafana 中将它们作为单独的列进行查看。

logger.LogAttrs(
    ctx,
    slog.LevelInfo, "order placed",
    slog.String("app.order.id", orderID.String()),
    slog.Float64("app.shipping.amount", shippingCostFloat),
    slog.Float64("app.order.amount", totalPriceFloat),
    slog.Int("app.order.items.count", len(prep.orderItems)),
    slog.String("app.shipping.tracking.id", shippingTrackingID),
)

最后修改日期:2025 年 8 月 8 日: Adding logging docs for Checkout service (#7507) (f9a0439a)