Go로 trace 및 측정항목 생성

이 문서에서는 오픈소스 OpenTelemetry 프레임워크를 사용하여 trace 및 측정항목 데이터를 수집하도록 Go 앱을 수정하는 방법과 구조화된 JSON 로그를 표준 출력에 작성하는 방법을 설명합니다. 설치 및 실행할 수 있는 샘플 앱에 대한 정보도 제공합니다. 이 앱은 측정항목, trace, 로그를 생성하도록 구성됩니다.

컨텍스트 정보

OpenTelemetry의 컨텍스트는 프로세스 내의 API 간에 실행 범위 값을 전달하는 메커니즘입니다. 컨텍스트의 중요한 용도는 생성 시 새 스팬을 수정하거나 상위 스팬으로 참조할 수 있도록 현재 활성 스팬을 전달하는 것입니다. 요약

  • 컨텍스트는 프로세스 내의 API에 현재 활성 스팬을 포함한 실행 범위 값을 전파하는 메커니즘을 나타냅니다.

  • 스팬 컨텍스트는 trace ID, 스팬 ID, trace의 플래그 및 상태를 포함하는 모든 스팬에서 변경할 수 없는 객체입니다.

  • 전파는 서비스와 프로세스 간에 컨텍스트를 이동하는 메커니즘입니다.

Go 표준 라이브러리의 context.Context도 API 경계를 넘어 범위가 지정된 값을 전달합니다. 일반적으로 서버의 핸들러 함수는 수신 Context를 수신하여 호출 체인을 통해 발신 요청을 수행하는 모든 클라이언트에 전달합니다.

Go의 표준 라이브러리 context.Context는 Go에서 OpenTelemetry 컨텍스트의 구현으로 사용됩니다.

시작하기 전에

API Cloud Logging API, Cloud Monitoring API, and Cloud Trace API 사용 설정

API 사용 설정

앱을 계측하여 trace, 측정항목, 로그 수집

trace 및 측정항목 데이터를 수집하고 구조화된 JSON을 표준 출력에 작성하도록 앱을 계측하려면 이 문서의 이어지는 섹션에 설명된 대로 다음 단계를 수행합니다.

  1. 기본 함수 구성
  2. OpenTelemetry 구성
  3. 구조화된 로깅 구성
  4. HTTP 서버에 계측 추가
  5. trace 스팬을 로그 및 측정항목과 연결
  6. HTTP 클라이언트에 계측 추가
  7. 구조화된 로그 작성

기본 함수 구성

OpenTelemetry를 사용하여 구조화된 로그를 작성하고 측정항목 및 trace 데이터를 수집하도록 앱을 구성하려면 Go의 구조화된 로깅 패키지인 slog를 구성하고 OpenTelemetry를 구성하도록 main 함수를 업데이트합니다.

다음 코드 샘플은 두 개의 도우미 함수 setupLogging()setupOpenTelemetry()를 호출하는 main 함수를 보여줍니다. 이러한 도우미 함수는 로깅 패키지 및 OpenTelemetry를 구성합니다.

func main() {
	ctx := context.Background()

	// Setup logging
	setupLogging()

	// Setup metrics, tracing, and context propagation
	shutdown, err := setupOpenTelemetry(ctx)
	if err != nil {
		slog.ErrorContext(ctx, "error setting up OpenTelemetry", slog.Any("error", err))
		os.Exit(1)
	}

	// Run the http server, and shutdown and flush telemetry after it exits.
	slog.InfoContext(ctx, "server starting...")
	if err = errors.Join(runServer(), shutdown(ctx)); err != nil {
		slog.ErrorContext(ctx, "server exited with error", slog.Any("error", err))
		os.Exit(1)
	}
}

로깅 패키지를 구성한 후 로그를 trace 데이터에 연결하려면 로거에 Go Context를 전달해야 합니다. 자세한 내용은 이 문서의 구조화된 로그 작성 섹션을 참조하세요.

OpenTelemetry 구성

OTLP 프로토콜을 사용하여 trace 및 측정항목을 수집하고 내보내려면 전역 TracerProviderMeterProvider 인스턴스를 구성합니다. 다음 코드 샘플은 main 함수에서 호출되는 setupOpenTelemetry 함수를 보여줍니다.

func setupOpenTelemetry(ctx context.Context) (shutdown func(context.Context) error, err error) {
	var shutdownFuncs []func(context.Context) error

	// shutdown combines shutdown functions from multiple OpenTelemetry
	// components into a single function.
	shutdown = func(ctx context.Context) error {
		var err error
		for _, fn := range shutdownFuncs {
			err = errors.Join(err, fn(ctx))
		}
		shutdownFuncs = nil
		return err
	}

	// Configure Context Propagation to use the default W3C traceparent format
	otel.SetTextMapPropagator(autoprop.NewTextMapPropagator())

	// Configure Trace Export to send spans as OTLP
	texporter, err := autoexport.NewSpanExporter(ctx)
	if err != nil {
		err = errors.Join(err, shutdown(ctx))
		return
	}
	tp := trace.NewTracerProvider(trace.WithBatcher(texporter))
	shutdownFuncs = append(shutdownFuncs, tp.Shutdown)
	otel.SetTracerProvider(tp)

	// Configure Metric Export to send metrics as OTLP
	mreader, err := autoexport.NewMetricReader(ctx)
	if err != nil {
		err = errors.Join(err, shutdown(ctx))
		return
	}
	mp := metric.NewMeterProvider(
		metric.WithReader(mreader),
	)
	shutdownFuncs = append(shutdownFuncs, mp.Shutdown)
	otel.SetMeterProvider(mp)

	return shutdown, nil
}

이전 코드 샘플은 trace 컨텍스트 전파W3C Trace 컨텍스트 형식을 사용하도록 전역 TextMapPropagator를 구성합니다. 이 구성을 통해 스팬이 trace 내에서 올바른 상위-하위 관계를 갖도록 할 수 있습니다.

대기 중인 모든 원격 분석이 플러시되고 연결이 단계적으로 종료되도록 setupOpenTelemetry 함수는 이러한 작업을 수행하는 shutdown이라는 함수를 반환합니다.

구조화된 로깅 구성

표준 출력에 작성된 JSON 형식 로그의 일부로 trace 정보를 포함하려면 Go의 구조화된 로깅 패키지 slog를 구성합니다. 다음 코드 샘플은 main 함수에서 호출되는 setupLogging 함수를 보여줍니다.

func setupLogging() {
	// Use json as our base logging format.
	jsonHandler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ReplaceAttr: replacer})
	// Add span context attributes when Context is passed to logging calls.
	instrumentedHandler := handlerWithSpanContext(jsonHandler)
	// Set this handler as the global slog handler.
	slog.SetDefault(slog.New(instrumentedHandler))
}

이전 코드는 handlerWithSpanContext 함수를 호출하여 Context 인스턴스에서 정보를 추출하고 이 정보를 로그에 속성으로 추가합니다. 그런 다음, 다음 속성을 사용하여 로그와 trace의 상관관계를 보여줄 수 있습니다.

  • logging.googleapis.com/trace: 로그 항목과 연결된 trace의 리소스 이름입니다.
  • logging.googleapis.com/spanId: 로그 항목과 연결된 trace가 있는 스팬 ID입니다.
  • logging.googleapis.com/trace_sampled: 이 필드의 값은 true 또는 false여야 합니다.

이러한 필드에 대한 자세한 내용은 LogEntry 구조를 참조하세요.

func handlerWithSpanContext(handler slog.Handler) *spanContextLogHandler {
	return &spanContextLogHandler{Handler: handler}
}

// spanContextLogHandler is an slog.Handler which adds attributes from the
// span context.
type spanContextLogHandler struct {
	slog.Handler
}

// Handle overrides slog.Handler's Handle method. This adds attributes from the
// span context to the slog.Record.
func (t *spanContextLogHandler) Handle(ctx context.Context, record slog.Record) error {
	// Get the SpanContext from the golang Context.
	if s := trace.SpanContextFromContext(ctx); s.IsValid() {
		// Add trace context attributes following Cloud Logging structured log format described
		// in https://cloud.google.com/logging/docs/structured-logging#special-payload-fields
		record.AddAttrs(
			slog.Any("logging.googleapis.com/trace", s.TraceID()),
		)
		record.AddAttrs(
			slog.Any("logging.googleapis.com/spanId", s.SpanID()),
		)
		record.AddAttrs(
			slog.Bool("logging.googleapis.com/trace_sampled", s.TraceFlags().IsSampled()),
		)
	}
	return t.Handler.Handle(ctx, record)
}

func replacer(groups []string, a slog.Attr) slog.Attr {
	// Rename attribute keys to match Cloud Logging structured log format
	switch a.Key {
	case slog.LevelKey:
		a.Key = "severity"
		// Map slog.Level string values to Cloud Logging LogSeverity
		// https://cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#LogSeverity
		if level := a.Value.Any().(slog.Level); level == slog.LevelWarn {
			a.Value = slog.StringValue("WARNING")
		}
	case slog.TimeKey:
		a.Key = "timestamp"
	case slog.MessageKey:
		a.Key = "message"
	}
	return a
}

HTTP 서버에 계측 추가

HTTP 서버에서 처리되는 요청에 trace 및 측정항목 계측을 추가하려면 OpenTelemetry를 사용합니다. 다음 샘플에서는 컨텍스트를 전파하고 trace 및 측정항목 계측을 위해 otelhttp 핸들러를 사용합니다.

func runServer() error {
	handleHTTP("/single", handleSingle)
	handleHTTP("/multi", handleMulti)

	return http.ListenAndServe(":8080", nil)
}

// handleHTTP handles the http HandlerFunc on the specified route, and uses
// otelhttp for context propagation, trace instrumentation, and metric
// instrumentation.
func handleHTTP(route string, handleFn http.HandlerFunc) {
	instrumentedHandler := otelhttp.NewHandler(otelhttp.WithRouteTag(route, handleFn), route)

	http.Handle(route, instrumentedHandler)
}

이전 코드에서 otelhttp 핸들러는 전역 TracerProvider, MeterProvider, TextMapPropagator 인스턴스를 사용합니다. setupOpenTelemetry 함수는 이러한 인스턴스를 구성합니다.

trace 스팬을 로그 및 측정항목과 연결

서버 및 클라이언트 스팬을 연결하고 측정항목 및 로그를 연결하려면 로그를 작성할 때 Go Context 인스턴스를 HTTP 요청에 전달합니다. 다음 예시는 Go Context 인스턴스를 추출하고 이 인스턴스를 로거와 callSingle 함수에 전달하여 발신 HTTP 요청을 수행하는 경로 핸들러를 보여줍니다.

func handleMulti(w http.ResponseWriter, r *http.Request) {
	subRequests := 3 + rand.Intn(4)
	// Write a structured log with the request context, which allows the log to
	// be linked with the trace for this request.
	slog.InfoContext(r.Context(), "handle /multi request", slog.Int("subRequests", subRequests))

	// Make 3-7 http requests to the /single endpoint.
	for i := 0; i < subRequests; i++ {
		if err := callSingle(r.Context()); err != nil {
			http.Error(w, err.Error(), http.StatusBadGateway)
			return
		}
	}

	fmt.Fprintln(w, "ok")
}

이전 코드에서 함수 호출 r.Context()는 HTTP 요청에서 Go Context를 검색합니다.

HTTP 클라이언트에 계측 추가

trace 컨텍스트를 발신 HTTP 요청에 삽입하고 trace 및 측정항목 계측을 추가하려면 otelhttp.Get 함수를 호출합니다. 다음 예시에서 callSingle 함수는 이 작업을 수행합니다.

func callSingle(ctx context.Context) error {
	// otelhttp.Get makes an http GET request, just like net/http.Get.
	// In addition, it records a span, records metrics, and propagates context.
	res, err := otelhttp.Get(ctx, "http://localhost:8080/single")
	if err != nil {
		return err
	}

	return res.Body.Close()
}

이전 코드에서 otelhttp 핸들러는 전역 TracerProvider, MeterProvider, TextMapPropagator 인스턴스를 사용합니다. setupOpenTelemetry 함수는 이러한 인스턴스를 구성합니다.

구조화된 로그 작성

trace에 연결되는 구조화된 로그를 작성하려면 Go의 구조화된 로깅 패키지인 slog를 사용하고 Go Context 인스턴스를 로거에 전달합니다. 로그를 스팬에 연결하려면 Go Context 인스턴스가 필요합니다. 예를 들어 다음 문은 slog에 대해 InfoContext 메서드를 호출하는 방법과 subRequests 필드를 JSON 인스턴스에 추가하는 방법을 보여줍니다.

slog.InfoContext(r.Context(), "handle /multi request", slog.Int("subRequests", subRequests))

원격 분석을 수집하도록 구성된 샘플 앱 실행

예시 앱은 로그용 JSON, 측정항목 및 trace용 OTLP를 포함하여 공급업체 중립적인 형식을 사용합니다. 원격 분석을 Google Cloud로 라우팅하기 위해 이 샘플은 Google 내보내기 도구로 구성된 OpenTelemetry Collector를 사용합니다. 앱의 부하 생성기는 앱의 경로로 요청을 보냅니다.

앱 다운로드 및 배포

샘플을 실행하려면 다음을 수행합니다.

  1. Google Cloud 콘솔에서 Cloud Shell을 활성화합니다.

    Cloud Shell 활성화

    Google Cloud 콘솔 하단에서 Cloud Shell 세션이 시작되고 명령줄 프롬프트가 표시됩니다. Cloud Shell은 Google Cloud CLI가 사전 설치된 셸 환경으로, 현재 프로젝트의 값이 이미 설정되어 있습니다. 세션이 초기화되는 데 몇 초 정도 걸릴 수 있습니다.

  2. 저장소를 복제합니다.

    git clone https://github.com/GoogleCloudPlatform/golang-samples
    
  3. OpenTelemetry 디렉터리로 이동합니다.

    cd golang-samples/opentelemetry/instrumentation
    
  4. 샘플을 빌드하고 실행합니다.

    docker compose up --abort-on-container-exit
    

    Cloud Shell에서 실행하지 않는 경우 사용자 인증 정보 파일을 가리키는 GOOGLE_APPLICATION_CREDENTIALS 환경 변수를 사용하여 애플리케이션을 실행합니다. 애플리케이션 기본 사용자 인증 정보$HOME/.config/gcloud/application_default_credentials.json에서 사용자 인증 정보 파일을 제공합니다.

    # Set environment variables
    export GOOGLE_CLOUD_PROJECT="PROJECT_ID"
    export GOOGLE_APPLICATION_CREDENTIALS="$HOME/.config/gcloud/application_default_credentials.json"
    export USERID="$(id -u)"
    
    # Run
    docker compose -f docker-compose.yaml -f docker-compose.creds.yaml up --abort-on-container-exit
    

측정항목 보기

샘플 앱의 OpenTelemetry 계측은 측정항목 탐색기를 사용하여 볼 수 있는 Prometheus 측정항목을 생성합니다.

  • Prometheus/http_server_duration/histogram은 서버 요청 기간을 기록하고 결과를 히스토그램에 저장합니다.

  • Prometheus/http_server_request_content_length_total/counter/multi/single HTTP 경로의 요청 콘텐츠 길이를 기록합니다. 이 측정항목의 측정값은 누적됩니다. 즉, 각 값은 값 수집이 시작된 이후의 합계를 나타냅니다.

  • Prometheus/http_server_response_content_length_total/counter/multi/single HTTP 경로의 응답 콘텐츠 길이를 기록합니다. 이 측정항목의 측정값은 누적됩니다.

샘플 앱에서 생성된 측정항목을 보려면 다음을 실행합니다.
  1. Google Cloud 콘솔의 탐색 패널에서 Monitoring을 선택한 후 측정항목 탐색기를 선택합니다.

    측정항목 탐색기로 이동

  2. 측정항목 요소에서 측정항목 선택 메뉴를 펼치고 필터 표시줄에 http_server을 입력한 후 하위 메뉴를 사용하여 특정 리소스 유형과 측정항목을 선택합니다.
    1. 활성 리소스 메뉴에서 Prometheus 대상을 선택합니다.
    2. 활성 측정항목 카테고리 메뉴에서 Http를 선택합니다.
    3. 활성 측정항목 메뉴에서 측정항목을 선택합니다.
    4. 적용을 클릭합니다.
  3. 데이터를 보는 방법을 구성합니다.

    측정항목의 측정값이 누적되면 측정항목 탐색기는 측정된 데이터를 정렬 기간에 따라 자동으로 정규화하므로 차트에 비율이 표시됩니다. 자세한 내용은 종류, 유형, 변환을 참조하세요.

    두 개의 counter 측정항목과 같이 정수 값 또는 Double 값이이 측정되면 측정항목 탐색기가 모든 시계열을 자동으로 합산합니다. /multi/single HTTP 경로의 데이터를 보려면 집계 항목의 첫 번째 메뉴를 없음으로 설정합니다.

    차트 구성에 대한 자세한 내용은 측정항목 탐색기 사용 시 측정항목 선택을 참조하세요.

trace 보기

trace 데이터를 보려면 다음을 수행합니다.

  1. Google Cloud 콘솔의 탐색 패널에서 Trace를 선택한 후 Trace 탐색기를 선택합니다.

    Trace 탐색기로 이동

  2. 분산형 차트에서 URI가 /multi인 trace를 선택합니다.
  3. trace 세부정보 패널의 Gantt 차트에서 /multi 라벨이 지정된 스팬을 선택합니다.

    HTTP 요청에 대한 정보가 표시된 패널이 열립니다. 이러한 세부정보에는 메서드, 상태 코드, 바이트 수, 호출자의 사용자 에이전트가 포함됩니다.

  4. 이 trace와 연결된 로그를 보려면 로그 및 이벤트 탭을 선택합니다.

    탭에는 개별 로그가 표시됩니다. 로그 항목의 세부정보를 보려면 로그 항목을 펼칩니다. 로그 보기를 클릭하고 로그 탐색기를 사용하여 로그를 볼 수도 있습니다.

Cloud Trace 탐색기 사용에 대한 자세한 내용은 trace 찾기 및 탐색을 참조하세요.

로그 보기

로그 탐색기에서 로그를 검사할 수 있으며 연결된 trace가 있는 경우 이를 볼 수도 있습니다.

  1. Google Cloud 콘솔의 탐색 패널에서 Logging을 선택한 후 로그 탐색기를 선택합니다.

    로그 탐색기로 이동

  2. handle /multi request라는 설명이 포함된 로그를 찾습니다.

    로그 세부정보를 보려면 로그 항목을 확장합니다. jsonPayload 필드에 subRequests라는 라벨이 지정된 항목이 있습니다. 이 항목은 handleMulti 함수의 문으로 추가되었습니다.

  3. 'handle /multi request' 메시지가 있는 로그 항목에서 trace를 클릭한 후 trace 세부정보 보기를 선택합니다.

    trace 세부정보 패널이 열리고 선택한 trace가 표시됩니다.

로그 탐색기 사용에 대한 자세한 내용은 로그 탐색기를 사용하여 로그 보기를 참조하세요.

다음 단계