diff --git a/.readme-partials.yaml b/.readme-partials.yaml index 0da4b073838..09b3632d24f 100644 --- a/.readme-partials.yaml +++ b/.readme-partials.yaml @@ -153,8 +153,9 @@ custom_content: | #### OpenTelemetry SQL Statement Tracing The OpenTelemetry traces that are generated by the Java client include any request and transaction - tags that have been set. The traces can also include the SQL statements that are executed. Enable - this with the `enableExtendedTracing` option: + tags that have been set. The traces can also include the SQL statements that are executed and the + name of the thread that executes the statement. Enable this with the `enableExtendedTracing` + option: ``` SpannerOptions options = SpannerOptions.newBuilder() diff --git a/README.md b/README.md index 81c0d3bdbbc..979d8832013 100644 --- a/README.md +++ b/README.md @@ -57,13 +57,13 @@ implementation 'com.google.cloud:google-cloud-spanner' If you are using Gradle without BOM, add this to your dependencies: ```Groovy -implementation 'com.google.cloud:google-cloud-spanner:6.69.0' +implementation 'com.google.cloud:google-cloud-spanner:6.70.0' ``` If you are using SBT, add this to your dependencies: ```Scala -libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.69.0" +libraryDependencies += "com.google.cloud" % "google-cloud-spanner" % "6.70.0" ``` @@ -259,8 +259,9 @@ Spanner spanner = options.getService(); #### OpenTelemetry SQL Statement Tracing The OpenTelemetry traces that are generated by the Java client include any request and transaction -tags that have been set. The traces can also include the SQL statements that are executed. Enable -this with the `enableExtendedTracing` option: +tags that have been set. The traces can also include the SQL statements that are executed and the +name of the thread that executes the statement. Enable this with the `enableExtendedTracing` +option: ``` SpannerOptions options = SpannerOptions.newBuilder() @@ -687,7 +688,7 @@ Java is a registered trademark of Oracle and/or its affiliates. [kokoro-badge-link-5]: http://storage.googleapis.com/cloud-devrel-public/java/badges/java-spanner/java11.html [stability-image]: https://img.shields.io/badge/stability-stable-green [maven-version-image]: https://img.shields.io/maven-central/v/com.google.cloud/google-cloud-spanner.svg -[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.69.0 +[maven-version-link]: https://central.sonatype.com/artifact/com.google.cloud/google-cloud-spanner/6.70.0 [authentication]: https://github.com/googleapis/google-cloud-java#authentication [auth-scopes]: https://developers.google.com/identity/protocols/oauth2/scopes [predefined-iam-roles]: https://cloud.google.com/iam/docs/understanding-roles#predefined_roles diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenTelemetryContextKeys.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenTelemetryContextKeys.java new file mode 100644 index 00000000000..e5fbedb7c37 --- /dev/null +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/OpenTelemetryContextKeys.java @@ -0,0 +1,30 @@ +/* + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.google.cloud.spanner; + +import com.google.api.core.InternalApi; +import io.opentelemetry.context.ContextKey; + +/** + * Keys for OpenTelemetry context variables that are used by the Spanner client library. Only + * intended for internal use. + */ +@InternalApi +public class OpenTelemetryContextKeys { + @InternalApi + public static final ContextKey THREAD_NAME_KEY = ContextKey.named("thread.name"); +} diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java index c3e75a7ed54..fa93f768baa 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/SpannerOptions.java @@ -1377,6 +1377,7 @@ public Builder setEnableApiTracing(boolean enableApiTracing) { * * */ public Builder setEnableExtendedTracing(boolean enableExtendedTracing) { @@ -1667,6 +1668,7 @@ public boolean isUseVirtualThreads() { * * */ public boolean isEnableExtendedTracing() { diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java index 77dbc010c4d..02638445ae2 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/TraceWrapper.java @@ -19,6 +19,7 @@ import com.google.cloud.spanner.Options.TagOption; import com.google.cloud.spanner.Options.TransactionOption; import com.google.cloud.spanner.SpannerOptions.TracingFramework; +import com.google.common.base.MoreObjects; import io.opencensus.trace.BlankSpan; import io.opencensus.trace.Span; import io.opencensus.trace.Tracer; @@ -41,6 +42,7 @@ class TraceWrapper { AttributeKey.stringKey("db.statement"); private static final AttributeKey> DB_STATEMENT_ARRAY_KEY = AttributeKey.stringArrayKey("db.statement"); + private static final AttributeKey THREAD_NAME_KEY = AttributeKey.stringKey("thread.name"); private final Tracer openCensusTracer; private final io.opentelemetry.api.trace.Tracer openTelemetryTracer; @@ -154,6 +156,7 @@ Attributes createStatementAttributes(Statement statement, Options options) { AttributesBuilder builder = Attributes.builder(); if (this.enableExtendedTracing) { builder.put(DB_STATEMENT_KEY, statement.getSql()); + builder.put(THREAD_NAME_KEY, getTraceThreadName()); } if (options != null && options.hasTag()) { builder.put(STATEMENT_TAG_KEY, options.tag()); @@ -172,6 +175,7 @@ Attributes createStatementBatchAttributes(Iterable statements, Option StreamSupport.stream(statements.spliterator(), false) .map(Statement::getSql) .collect(Collectors.toList())); + builder.put(THREAD_NAME_KEY, getTraceThreadName()); } if (options != null && options.hasTag()) { builder.put(STATEMENT_TAG_KEY, options.tag()); @@ -180,4 +184,10 @@ Attributes createStatementBatchAttributes(Iterable statements, Option } return Attributes.empty(); } + + private static String getTraceThreadName() { + return MoreObjects.firstNonNull( + Context.current().get(OpenTelemetryContextKeys.THREAD_NAME_KEY), + Thread.currentThread().getName()); + } } diff --git a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java index ff159c681c5..9e431dbc0ba 100644 --- a/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java +++ b/google-cloud-spanner/src/main/java/com/google/cloud/spanner/connection/AbstractBaseUnitOfWork.java @@ -25,6 +25,7 @@ import com.google.cloud.spanner.BatchTransactionId; import com.google.cloud.spanner.Dialect; import com.google.cloud.spanner.ErrorCode; +import com.google.cloud.spanner.OpenTelemetryContextKeys; import com.google.cloud.spanner.Options.QueryOption; import com.google.cloud.spanner.Options.RpcPriority; import com.google.cloud.spanner.Partition; @@ -47,6 +48,7 @@ import io.grpc.Status; import io.opentelemetry.api.common.AttributeKey; import io.opentelemetry.api.trace.Span; +import io.opentelemetry.context.Scope; import java.util.Collection; import java.util.Collections; import java.util.HashSet; @@ -358,40 +360,47 @@ public ApiCallContext configure( } }); } - ApiFuture f = statementExecutor.submit(context.wrap(callable)); - final SpannerAsyncExecutionException caller = - callType == CallType.ASYNC - ? new SpannerAsyncExecutionException(statement.getStatement()) - : null; - final ApiFuture future = - ApiFutures.catching( - f, - Throwable.class, - input -> { - if (caller != null) { - input.addSuppressed(caller); + // Register the name of the thread that called this method as the thread name that should be + // traced. + try (Scope ignore = + io.opentelemetry.context.Context.current() + .with(OpenTelemetryContextKeys.THREAD_NAME_KEY, Thread.currentThread().getName()) + .makeCurrent()) { + ApiFuture f = statementExecutor.submit(context.wrap(callable)); + final SpannerAsyncExecutionException caller = + callType == CallType.ASYNC + ? new SpannerAsyncExecutionException(statement.getStatement()) + : null; + final ApiFuture future = + ApiFutures.catching( + f, + Throwable.class, + input -> { + if (caller != null) { + input.addSuppressed(caller); + } + throw SpannerExceptionFactory.asSpannerException(input); + }, + MoreExecutors.directExecutor()); + synchronized (this) { + this.currentlyRunningStatementFuture = future; + } + future.addListener( + new Runnable() { + @Override + public void run() { + synchronized (this) { + if (currentlyRunningStatementFuture == future) { + currentlyRunningStatementFuture = null; + } } - throw SpannerExceptionFactory.asSpannerException(input); - }, - MoreExecutors.directExecutor()); - synchronized (this) { - this.currentlyRunningStatementFuture = future; - } - future.addListener( - new Runnable() { - @Override - public void run() { - synchronized (this) { - if (currentlyRunningStatementFuture == future) { - currentlyRunningStatementFuture = null; + if (isSingleUse()) { + endUnitOfWorkSpan(); } } - if (isSingleUse()) { - endUnitOfWorkSpan(); - } - } - }, - MoreExecutors.directExecutor()); - return future; + }, + MoreExecutors.directExecutor()); + return future; + } } } diff --git a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/OpenTelemetryTracingTest.java b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/OpenTelemetryTracingTest.java index bc38761d75e..a8e579feb1a 100644 --- a/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/OpenTelemetryTracingTest.java +++ b/google-cloud-spanner/src/test/java/com/google/cloud/spanner/connection/OpenTelemetryTracingTest.java @@ -164,6 +164,14 @@ public boolean isEnableExtendedTracing() { "CloudSpannerOperation.ExecuteStreamingQuery", Attributes.of(AttributeKey.stringKey("db.statement"), SELECT1_STATEMENT.getSql()), spans); + SpanData executeQuerySpan = + getSpan( + "CloudSpannerOperation.ExecuteStreamingQuery", + Attributes.of( + AttributeKey.stringKey("db.statement"), SELECT1_STATEMENT.getSql(), + AttributeKey.stringKey("thread.name"), Thread.currentThread().getName()), + spans); + assertParent( "CloudSpannerJdbc.SingleUseTransaction", "CloudSpanner.ReadOnlyTransaction", spans); assertParent( @@ -190,6 +198,14 @@ public void testSingleUseQuery() { "CloudSpannerOperation.ExecuteStreamingQuery", Attributes.of(AttributeKey.stringKey("db.statement"), SELECT1_STATEMENT.getSql()), spans); + SpanData executeQuerySpan = + getSpan( + "CloudSpannerOperation.ExecuteStreamingQuery", + Attributes.of( + AttributeKey.stringKey("db.statement"), SELECT1_STATEMENT.getSql(), + AttributeKey.stringKey("thread.name"), Thread.currentThread().getName()), + spans); + assertParent( "CloudSpannerJdbc.SingleUseTransaction", "CloudSpanner.ReadOnlyTransaction", spans); assertParent( @@ -222,6 +238,13 @@ public void testSingleUseUpdate() { "CloudSpannerOperation.ExecuteUpdate", Attributes.of(AttributeKey.stringKey("db.statement"), INSERT_STATEMENT.getSql()), spans); + SpanData executeQuerySpan = + getSpan( + "CloudSpannerOperation.ExecuteUpdate", + Attributes.of( + AttributeKey.stringKey("db.statement"), INSERT_STATEMENT.getSql(), + AttributeKey.stringKey("thread.name"), Thread.currentThread().getName()), + spans); assertParent("CloudSpanner.ReadWriteTransaction", "CloudSpannerOperation.Commit", spans); } @@ -244,6 +267,15 @@ public void testSingleUseBatchUpdate() { AttributeKey.stringArrayKey("db.statement"), ImmutableList.of(INSERT_STATEMENT.getSql(), INSERT_STATEMENT.getSql())), spans); + SpanData executeQuerySpan = + getSpan( + "CloudSpannerOperation.BatchUpdate", + Attributes.of( + AttributeKey.stringArrayKey("db.statement"), + ImmutableList.of(INSERT_STATEMENT.getSql(), INSERT_STATEMENT.getSql())), + spans); + String threadName = executeQuerySpan.getAttributes().get(AttributeKey.stringKey("thread.name")); + assertEquals(Thread.currentThread().getName(), threadName); assertContains("CloudSpannerOperation.Commit", spans); assertParent(