OpenTelemetry를 사용하여 커스텀 클라이언트 측 측정항목 캡처

이 문서에서는 OpenTelemetry를 사용하여 커스텀 클라이언트 측정항목을 캡처하는 방법을 설명합니다. 맞춤 클라이언트 측정항목은 JavaGo 클라이언트 라이브러리를 사용하여 사용할 수 있습니다.

맞춤 클라이언트 측 측정항목을 사용하면 시스템에서 지연 시간의 원인을 찾을 수 있습니다. 자세한 내용은 Spanner 요청의 지연 시간 지점을 참고하세요.

Spanner 클라이언트 라이브러리는 OpenTelemetry 관측 가능성 프레임워크를 사용하여 통계와 trace도 제공합니다. 자세한 내용은 OpenTelemetry를 사용하여 trace 수집 설정을 참고하세요.

OpenTelemetry는 trace, 측정항목, 로그와 같은 원격 분석 데이터를 만들고 관리할 수 있게 해주는 오픈소스 관측 가능성 프레임워크 및 툴킷입니다.

시작하기 전에

원격 분석 데이터를 내보내는 데 적절한 옵션으로 OpenTelemetry SDK를 구성해야 합니다. OpenTelemetry 프로토콜 (OTLP) 내보내기 도구를 사용하는 것이 좋습니다.

OpenTelemetry를 사용하여 커스텀 클라이언트 측 측정항목을 설정하려면 OpenTelemetry SDK 및 OTLP 내보내기 도구를 구성해야 합니다.

  1. 다음 코드를 사용하여 애플리케이션에 필요한 종속 항목을 추가합니다.

    자바

    <dependencyManagement>
      <dependencies>
        <dependency>
          <groupId>com.google.cloud</groupId>
          <artifactId>libraries-bom</artifactId>
          <version>26.32.0</version>
          <type>pom</type>
          <scope>import</scope>
        </dependency>
        <dependency>
          <groupId>io.opentelemetry</groupId>
          <artifactId>opentelemetry-bom</artifactId>
          <version>1.35.0</version>
          <type>pom</type>
          <scope>import</scope>
        </dependency>
      </dependencies>
    </dependencyManagement>
    
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>google-cloud-spanner</artifactId>
      </dependency>
      <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-sdk</artifactId>
      </dependency>
      <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-sdk-metrics</artifactId>
      </dependency>
      <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-sdk-trace</artifactId>
      </dependency>
      <dependency>
        <groupId>io.opentelemetry</groupId>
        <artifactId>opentelemetry-exporter-otlp</artifactId>
      </dependency>
    </dependencies>

    Go

    go.opentelemetry.io/otel v1.24.0
    go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.23.1
    go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.23.1
    go.opentelemetry.io/otel/metric v1.24.0
    go.opentelemetry.io/otel/sdk v1.24.0
    go.opentelemetry.io/otel/sdk/metric v1.23.1
  2. OTLP 내보내기 도구를 사용하여 OpenTelemetry 객체를 만들고 SpannerOptions를 사용하여 Spanner에 삽입합니다.

    자바

    // Enable OpenTelemetry metrics and traces before Injecting OpenTelemetry
    SpannerOptions.enableOpenTelemetryMetrics();
    SpannerOptions.enableOpenTelemetryTraces();
    
    // Create a new meter provider
    SdkMeterProvider sdkMeterProvider = SdkMeterProvider.builder()
        // Use Otlp exporter or any other exporter of your choice.
        .registerMetricReader(
            PeriodicMetricReader.builder(OtlpGrpcMetricExporter.builder().build()).build())
        .build();
    
    // Create a new tracer provider
    SdkTracerProvider sdkTracerProvider = SdkTracerProvider.builder()
        // Use Otlp exporter or any other exporter of your choice.
        .addSpanProcessor(SimpleSpanProcessor.builder(OtlpGrpcSpanExporter
            .builder().build()).build())
            .build();
    
    // Configure OpenTelemetry object using Meter Provider and Tracer Provider
    OpenTelemetry openTelemetry = OpenTelemetrySdk.builder()
        .setMeterProvider(sdkMeterProvider)
        .setTracerProvider(sdkTracerProvider)
        .build();
    
    // Inject OpenTelemetry object via Spanner options or register as GlobalOpenTelemetry.
    SpannerOptions options = SpannerOptions.newBuilder()
        .setOpenTelemetry(openTelemetry)
        .build();
    Spanner spanner = options.getService();
    
    DatabaseClient dbClient = spanner
        .getDatabaseClient(DatabaseId.of(projectId, instanceId, databaseId));
    
    captureGfeMetric(dbClient);
    captureQueryStatsMetric(openTelemetry, dbClient);
    
    // Close the providers to free up the resources and export the data. */
    sdkMeterProvider.close();
    sdkTracerProvider.close();

    Go

    // Ensure that your Go runtime version is supported by the OpenTelemetry-Go compatibility policy before enabling OpenTelemetry instrumentation.
    // Refer to compatibility here https://github.com/googleapis/google-cloud-go/blob/main/debug.md#opentelemetry
    
    import (
    	"context"
    	"fmt"
    	"io"
    	"log"
    	"strconv"
    	"strings"
    
    	"cloud.google.com/go/spanner"
    	"go.opentelemetry.io/otel"
    	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
    	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
    	"go.opentelemetry.io/otel/metric"
    	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
    	"go.opentelemetry.io/otel/sdk/resource"
    	sdktrace "go.opentelemetry.io/otel/sdk/trace"
    	semconv "go.opentelemetry.io/otel/semconv/v1.24.0"
    	"google.golang.org/api/iterator"
    )
    
    func enableOpenTelemetryMetricsAndTraces(w io.Writer, db string) error {
    	// db = `projects/<project>/instances/<instance-id>/database/<database-id>`
    	ctx := context.Background()
    
    	// Create a new resource to uniquely identify the application
    	res, err := newResource()
    	if err != nil {
    		log.Fatal(err)
    	}
    
    	// Enable OpenTelemetry traces by setting environment variable GOOGLE_API_GO_EXPERIMENTAL_TELEMETRY_PLATFORM_TRACING to the case-insensitive value "opentelemetry" before loading the client library.
    	// Enable OpenTelemetry metrics before injecting meter provider.
    	spanner.EnableOpenTelemetryMetrics()
    
    	// Create a new tracer provider
    	tracerProvider, err := getOtlpTracerProvider(ctx, res)
    	defer tracerProvider.ForceFlush(ctx)
    	if err != nil {
    		log.Fatal(err)
    	}
    	// Register tracer provider as global
    	otel.SetTracerProvider(tracerProvider)
    
    	// Create a new meter provider
    	meterProvider := getOtlpMeterProvider(ctx, res)
    	defer meterProvider.ForceFlush(ctx)
    
    	// Inject meter provider locally via ClientConfig when creating a spanner client or set globally via setMeterProvider.
    	client, err := spanner.NewClientWithConfig(ctx, db, spanner.ClientConfig{OpenTelemetryMeterProvider: meterProvider})
    	if err != nil {
    		return err
    	}
    	defer client.Close()
    	return nil
    }
    
    func getOtlpMeterProvider(ctx context.Context, res *resource.Resource) *sdkmetric.MeterProvider {
    	otlpExporter, err := otlpmetricgrpc.New(ctx)
    	if err != nil {
    		log.Fatal(err)
    	}
    	meterProvider := sdkmetric.NewMeterProvider(
    		sdkmetric.WithResource(res),
    		sdkmetric.WithReader(sdkmetric.NewPeriodicReader(otlpExporter)),
    	)
    	return meterProvider
    }
    
    func getOtlpTracerProvider(ctx context.Context, res *resource.Resource) (*sdktrace.TracerProvider, error) {
    	traceExporter, err := otlptracegrpc.New(ctx)
    	if err != nil {
    		return nil, err
    	}
    
    	tracerProvider := sdktrace.NewTracerProvider(
    		sdktrace.WithResource(res),
    		sdktrace.WithBatcher(traceExporter),
    		sdktrace.WithSampler(sdktrace.AlwaysSample()),
    	)
    
    	return tracerProvider, nil
    }
    
    func newResource() (*resource.Resource, error) {
    	return resource.Merge(resource.Default(),
    		resource.NewWithAttributes(semconv.SchemaURL,
    			semconv.ServiceName("otlp-service"),
    			semconv.ServiceVersion("0.1.0"),
    		))
    }
    

GFE 지연 시간 캡처

Google 프런트엔드(GFE) 지연 시간은 Google 네트워크에서 클라이언트의 리모트 프로시져 콜을 수신한 후 GFE가 응답의 첫 번째 바이트를 수신할 때까지의 시간(밀리초)입니다.

다음 코드를 사용하여 GFE 지연 시간을 캡처할 수 있습니다.

자바

static void captureGfeMetric(DatabaseClient dbClient) {
  // GFE_latency and other Spanner metrics are automatically collected
  // when OpenTelemetry metrics are enabled.

  try (ResultSet resultSet =
      dbClient
          .singleUse() // Execute a single read or query against Cloud Spanner.
          .executeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"))) {
    while (resultSet.next()) {
      System.out.printf(
          "%d %d %s", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2));
    }
  }
}

Go

// GFE_Latency and other Spanner metrics are automatically collected
// when OpenTelemetry metrics are enabled.
func captureGFELatencyMetric(ctx context.Context, client spanner.Client) error {
	stmt := spanner.Statement{SQL: `SELECT SingerId, AlbumId, AlbumTitle FROM Albums`}
	iter := client.Single().Query(ctx, stmt)
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			return nil
		}
		if err != nil {
			return err
		}
		var singerID, albumID int64
		var albumTitle string
		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
			return err
		}
	}
}

이 코드 샘플은 Cloud Monitoring으로 내보낼 때 측정항목 이름에 문자열 spanner/gfe_latency를 추가합니다. 추가된 문자열을 사용하여 Cloud Monitoring에서 이 측정항목을 검색할 수 있습니다.

Cloud Spanner API 요청 지연 시간 캡처

Cloud Spanner API 요청 지연 시간은 Cloud Spanner API 프런트엔드에서 수신한 클라이언트 요청의 첫 번째 바이트와 Cloud Spanner API 프런트엔드에서 전송한 응답의 마지막 바이트 사이의 시간(초)입니다.

이 지연 시간 측정항목은 Cloud Monitoring 측정항목의 일부로 제공됩니다.

클라이언트 왕복 지연 시간 캡처

클라이언트 왕복 지연 시간은 클라이언트가 GFE 및 Cloud Spanner API 프런트엔드를 통해 데이터베이스로 전송한 Cloud Spanner API 요청의 첫 번째 바이트와 클라이언트가 데이터베이스에서 수신한 응답의 마지막 바이트 사이의 시간 (밀리초)입니다.

OpenTelemetry를 사용하는 경우 Spanner 클라이언트 왕복 지연 시간 측정항목은 지원되지 않습니다. 대신 작업 지연 시간 클라이언트 측 측정항목을 볼 수 있습니다. 자세한 내용은 클라이언트 측 측정항목 설명을 참고하세요.

브리지와 함께 OpenCensus를 사용하면 측정항목을 계측하고 데이터를 OpenTelemetry로 마이그레이션할 수도 있습니다.

쿼리 지연 시간 캡처

쿼리 지연 시간은 Spanner 데이터베이스에서 SQL 쿼리를 실행하는 데 걸리는 시간(밀리초)입니다.

다음 코드를 사용하여 쿼리 지연 시간을 캡처할 수 있습니다.

자바

static void captureQueryStatsMetric(OpenTelemetry openTelemetry, DatabaseClient dbClient) {
  // Register query stats metric.
  // This should be done once before start recording the data.
  Meter meter = openTelemetry.getMeter("cloud.google.com/java");
  DoubleHistogram queryStatsMetricLatencies =
      meter
          .histogramBuilder("spanner/query_stats_elapsed")
          .setDescription("The execution of the query")
          .setUnit("ms")
          .build();

  // Capture query stats metric data.
  try (ResultSet resultSet = dbClient.singleUse()
      .analyzeQuery(Statement.of("SELECT SingerId, AlbumId, AlbumTitle FROM Albums"),
          QueryAnalyzeMode.PROFILE)) {

    while (resultSet.next()) {
      System.out.printf(
          "%d %d %s", resultSet.getLong(0), resultSet.getLong(1), resultSet.getString(2));
    }

    String value = resultSet.getStats().getQueryStats()
        .getFieldsOrDefault("elapsed_time", Value.newBuilder().setStringValue("0 msecs").build())
        .getStringValue();
    double elapsedTime = value.contains("msecs")
        ? Double.parseDouble(value.replaceAll(" msecs", ""))
        : Double.parseDouble(value.replaceAll(" secs", "")) * 1000;
    queryStatsMetricLatencies.record(elapsedTime);
  }
}

Go

func captureQueryStatsMetric(ctx context.Context, mp metric.MeterProvider, client spanner.Client) error {
	meter := mp.Meter(spanner.OtInstrumentationScope)
	// Register query stats metric with OpenTelemetry to record the data.
	// This should be done once before start recording the data.
	queryStats, err := meter.Float64Histogram(
		"spanner/query_stats_elapsed",
		metric.WithDescription("The execution of the query"),
		metric.WithUnit("ms"),
		metric.WithExplicitBucketBoundaries(0.0, 0.01, 0.05, 0.1, 0.3, 0.6, 0.8, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 8.0, 10.0, 13.0,
			16.0, 20.0, 25.0, 30.0, 40.0, 50.0, 65.0, 80.0, 100.0, 130.0, 160.0, 200.0, 250.0,
			300.0, 400.0, 500.0, 650.0, 800.0, 1000.0, 2000.0, 5000.0, 10000.0, 20000.0, 50000.0,
			100000.0),
	)
	if err != nil {
		fmt.Print(err)
	}

	stmt := spanner.Statement{SQL: `SELECT SingerId, AlbumId, AlbumTitle FROM Albums`}
	iter := client.Single().QueryWithStats(ctx, stmt)
	defer iter.Stop()
	for {
		row, err := iter.Next()
		if err == iterator.Done {
			// Record query execution time with OpenTelemetry.
			elapasedTime := iter.QueryStats["elapsed_time"].(string)
			elapasedTimeMs, err := strconv.ParseFloat(strings.TrimSuffix(elapasedTime, " msecs"), 64)
			if err != nil {
				return err
			}
			queryStats.Record(ctx, elapasedTimeMs)
			return nil
		}
		if err != nil {
			return err
		}
		var singerID, albumID int64
		var albumTitle string
		if err := row.Columns(&singerID, &albumID, &albumTitle); err != nil {
			return err
		}
	}
}

이 코드 샘플은 Cloud Monitoring으로 내보낼 때 측정항목 이름에 문자열 spanner/query_stats_elapsed를 추가합니다. 추가된 문자열을 사용하여 Cloud Monitoring에서 이 측정항목을 검색할 수 있습니다.

측정항목 탐색기에서 측정항목 보기

  1. Google Cloud 콘솔에서 측정항목 탐색기 페이지로 이동합니다.

    측정항목 탐색기로 이동

  2. 프로젝트를 선택합니다.

  3. 측정항목 선택을 클릭합니다.

  4. 다음 문자열을 사용하여 지연 시간 측정항목을 검색합니다.

    • roundtrip_latency: 클라이언트 왕복 지연 시간 측정항목입니다.
    • spanner/gfe_latency: GFE 지연 시간 측정항목
    • spanner/query_stats_elapsed: 쿼리 지연 시간 측정항목
  5. 측정항목을 선택한 다음 적용을 클릭합니다.

측정항목 그룹화 또는 집계에 관한 자세한 내용은 메뉴를 사용하여 쿼리 빌드를 참고하세요.

다음 단계