주문 처리 서비스

이 서비스는 사용자의 주문을 처리하는 역할을 한다. 주문 처리 서비스는 주문을 처리하기 위해 여러 다른 서비스를 호출한다.

주문 처리 서비스 소스

트레이스

트레이싱 초기화하기

오픈텔레메트리(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()을 호출하는 것이 좋다. 이 서비스는 main의 지연 실행 함수(deferred function)에서 이를 호출한다.

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

gRPC 자동 계측 추가하기

이 서비스는 gRPC 요청을 수신하며, main 함수에서 gRPC 서버를 생성할 때 해당 요청에 대한 계측을 설정한다.

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

카프카 (Sarama) 자동 계측 추가하기

이 서비스는 처리 결과를 카프카(Kafka) 토픽에 기록하며, 다른 마이크로서비스가 이를 이어서 처리한다. 카프카 클라이언트를 계측하려면 프로듀서를 생성한 후 감싸야 한다.

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

자동 계측된 스팬에 속성 추가하기

자동 계측된 코드가 실행되는 동안 컨텍스트에서 현재 스팬을 가져올 수 있다.

span := trace.SpanFromContext(ctx)

스팬에 속성을 추가하려면 스팬 객체의 SetAttributes를 사용한다. PlaceOrder 함수에서는 스팬에 여러 속성을 추가한다.

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)),
)

스팬 이벤트 추가하기

스팬 이벤트를 추가하려면 스팬 객체의 AddEvent를 사용한다. PlaceOrder 함수에서는 여러 스팬 이벤트를 추가한다. 일부 이벤트에는 추가 속성이 있고, 나머지에는 없다.

속성 없이 스팬 이벤트 추가하기:

span.AddEvent("prepared")

추가 속성과 함께 스팬 이벤트 추가하기:

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

메트릭

메트릭 초기화하기

오픈텔레메트리 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)
    }
}()

Go 런타임 자동 계측 추가하기

Go 런타임은 main 함수에서 계측한다.

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

로그

두 가지 방법으로 오픈텔레메트리 컬렉터에 로그를 보낼 수 있다.

  • 컬렉터로 직접 전송
  • 파일 또는 stdout을 통해 전송

두 방법의 사용법은 수동 계측 문서의 로그 섹션에서 확인할 수 있다.

주문 처리 서비스는 컬렉터로 로그를 직접 보내며, 구조화된 로그를 출력하는 slog 로깅 패키지와 연결하는 로그 브리지(log bridge)를 사용하여 로그를 전송한다.

LoggerProvider 초기화

오픈텔레메트리 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 호출을 사용하여 컬렉터에 로그를 보낸다. 로그는 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를 사용하면 출력에 추가 속성을 첨부할 수 있다는 장점이 있다. 다음 예제는 orderID, shippingCost, totalPrice와 같은 몇 가지 속성을 첨부한다. 이를 통해 로그 출력의 일부로 이러한 속성을 확인하고 파싱할 수 있으며, 그라파나(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),
)